Remove serde derive for ShellError, replace via LabeledError (#12319)

# Description

This changes the interface for plugins to always represent errors as
`LabeledError`s. This is good for altlang plugins, as it would suck for
them to have to implement and track `ShellError`. We save a lot of
generated code from the `ShellError` serde impl too, so `nu` and plugins
get to have a smaller binary size.

Reduces the release binary size by 1.2 MiB on my build configuration.

# User-Facing Changes

- Changes plugin protocol. `ShellError` no longer serialized.
- `ShellError` serialize output is different
- `ShellError` no longer deserializes to exactly the same value as
serialized

# Tests + Formatting

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting

- [ ] Document in plugin protocol reference
This commit is contained in:
Devyn Cairns 2024-03-30 06:21:40 -07:00 committed by GitHub
parent cc39069e13
commit 714a0ccd24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 10 deletions

View File

@ -103,7 +103,8 @@ fn list_reader_recv_wrong_type() -> Result<(), ShellError> {
#[test] #[test]
fn reader_recv_raw_messages() -> Result<(), ShellError> { fn reader_recv_raw_messages() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::new(0, rx, TestSink::default()); let mut reader =
StreamReader::<Result<Vec<u8>, ShellError>, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::Raw(Ok(vec![10, 20]))))) tx.send(Ok(Some(StreamData::Raw(Ok(vec![10, 20])))))
.unwrap(); .unwrap();
@ -458,7 +459,7 @@ fn stream_manager_write_scenario() -> Result<(), ShellError> {
let expected_values = vec![b"hello".to_vec(), b"world".to_vec(), b"test".to_vec()]; let expected_values = vec![b"hello".to_vec(), b"world".to_vec(), b"test".to_vec()];
for value in &expected_values { for value in &expected_values {
writable.write(Ok(value.clone()))?; writable.write(Ok::<_, ShellError>(value.clone()))?;
} }
// Now try signalling ack // Now try signalling ack

View File

@ -184,8 +184,14 @@ fn read_pipeline_data_external_stream() -> Result<(), ShellError> {
test.add(StreamMessage::Data(14, Value::test_int(1).into())); test.add(StreamMessage::Data(14, Value::test_int(1).into()));
for _ in 0..iterations { for _ in 0..iterations {
test.add(StreamMessage::Data(12, Ok(out_pattern.clone()).into())); test.add(StreamMessage::Data(
test.add(StreamMessage::Data(13, Ok(err_pattern.clone()).into())); 12,
StreamData::Raw(Ok(out_pattern.clone())),
));
test.add(StreamMessage::Data(
13,
StreamData::Raw(Ok(err_pattern.clone())),
));
} }
test.add(StreamMessage::End(12)); test.add(StreamMessage::End(12));
test.add(StreamMessage::End(13)); test.add(StreamMessage::End(13));

View File

@ -236,7 +236,7 @@ impl From<StreamMessage> for PluginInput {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum StreamData { pub enum StreamData {
List(Value), List(Value),
Raw(Result<Vec<u8>, ShellError>), Raw(Result<Vec<u8>, LabeledError>),
} }
impl From<Value> for StreamData { impl From<Value> for StreamData {
@ -245,9 +245,15 @@ impl From<Value> for StreamData {
} }
} }
impl From<Result<Vec<u8>, LabeledError>> for StreamData {
fn from(value: Result<Vec<u8>, LabeledError>) -> Self {
StreamData::Raw(value)
}
}
impl From<Result<Vec<u8>, ShellError>> for StreamData { impl From<Result<Vec<u8>, ShellError>> for StreamData {
fn from(value: Result<Vec<u8>, ShellError>) -> Self { fn from(value: Result<Vec<u8>, ShellError>) -> Self {
StreamData::Raw(value) value.map_err(LabeledError::from).into()
} }
} }
@ -264,10 +270,10 @@ impl TryFrom<StreamData> for Value {
} }
} }
impl TryFrom<StreamData> for Result<Vec<u8>, ShellError> { impl TryFrom<StreamData> for Result<Vec<u8>, LabeledError> {
type Error = ShellError; type Error = ShellError;
fn try_from(data: StreamData) -> Result<Result<Vec<u8>, ShellError>, ShellError> { fn try_from(data: StreamData) -> Result<Result<Vec<u8>, LabeledError>, ShellError> {
match data { match data {
StreamData::Raw(value) => Ok(value), StreamData::Raw(value) => Ok(value),
StreamData::List(_) => Err(ShellError::PluginFailedToDecode { StreamData::List(_) => Err(ShellError::PluginFailedToDecode {
@ -277,6 +283,14 @@ impl TryFrom<StreamData> for Result<Vec<u8>, ShellError> {
} }
} }
impl TryFrom<StreamData> for Result<Vec<u8>, ShellError> {
type Error = ShellError;
fn try_from(value: StreamData) -> Result<Result<Vec<u8>, ShellError>, ShellError> {
Result::<Vec<u8>, LabeledError>::try_from(value).map(|res| res.map_err(ShellError::from))
}
}
/// A stream control or data message. /// A stream control or data message.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum StreamMessage { pub enum StreamMessage {

View File

@ -3,13 +3,14 @@ use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
ast::Operator, engine::StateWorkingSet, format_error, ParseError, Span, Spanned, Value, ast::Operator, engine::StateWorkingSet, format_error, LabeledError, ParseError, Span, Spanned,
Value,
}; };
/// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors
/// the evaluator might face, along with helpful spans to label. An error renderer will take this error value /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value
/// and pass it into an error viewer to display to the user. /// and pass it into an error viewer to display to the user.
#[derive(Debug, Clone, Error, Diagnostic, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Error, Diagnostic, PartialEq)]
pub enum ShellError { pub enum ShellError {
/// An operator received two arguments of incompatible types. /// An operator received two arguments of incompatible types.
/// ///
@ -1407,6 +1408,75 @@ impl From<super::LabeledError> for ShellError {
} }
} }
/// `ShellError` always serializes as [`LabeledError`].
impl Serialize for ShellError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
LabeledError::from_diagnostic(self).serialize(serializer)
}
}
/// `ShellError` always deserializes as if it were [`LabeledError`], resulting in a
/// [`ShellError::LabeledError`] variant.
impl<'de> Deserialize<'de> for ShellError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
LabeledError::deserialize(deserializer).map(ShellError::from)
}
}
pub fn into_code(err: &ShellError) -> Option<String> { pub fn into_code(err: &ShellError) -> Option<String> {
err.code().map(|code| code.to_string()) err.code().map(|code| code.to_string())
} }
#[test]
fn shell_error_serialize_roundtrip() {
// Ensure that we can serialize and deserialize `ShellError`, and check that it basically would
// look the same
let original_error = ShellError::CantConvert {
span: Span::new(100, 200),
to_type: "Foo".into(),
from_type: "Bar".into(),
help: Some("this is a test".into()),
};
println!("orig_error = {:#?}", original_error);
let serialized =
serde_json::to_string_pretty(&original_error).expect("serde_json::to_string_pretty failed");
println!("serialized = {}", serialized);
let deserialized: ShellError =
serde_json::from_str(&serialized).expect("serde_json::from_str failed");
println!("deserialized = {:#?}", deserialized);
// We don't expect the deserialized error to be the same as the original error, but its miette
// properties should be comparable
assert_eq!(original_error.to_string(), deserialized.to_string());
assert_eq!(
original_error.code().map(|c| c.to_string()),
deserialized.code().map(|c| c.to_string())
);
let orig_labels = original_error
.labels()
.into_iter()
.flatten()
.collect::<Vec<_>>();
let deser_labels = deserialized
.labels()
.into_iter()
.flatten()
.collect::<Vec<_>>();
assert_eq!(orig_labels, deser_labels);
assert_eq!(
original_error.help().map(|c| c.to_string()),
deserialized.help().map(|c| c.to_string())
);
}