mirror of
https://github.com/nushell/nushell.git
synced 2025-08-18 02:09:48 +02:00
Bidirectional communication and streams for plugins (#11911)
This commit is contained in:
@@ -1,33 +1,201 @@
|
||||
mod evaluated_call;
|
||||
mod plugin_custom_value;
|
||||
mod plugin_data;
|
||||
mod protocol_info;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util;
|
||||
|
||||
pub use evaluated_call::EvaluatedCall;
|
||||
use nu_protocol::{PluginSignature, ShellError, Span, Value};
|
||||
use nu_protocol::{PluginSignature, RawStream, ShellError, Span, Spanned, Value};
|
||||
pub use plugin_custom_value::PluginCustomValue;
|
||||
pub use plugin_data::PluginData;
|
||||
pub(crate) use protocol_info::ProtocolInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CallInfo {
|
||||
#[cfg(test)]
|
||||
pub(crate) use protocol_info::Protocol;
|
||||
|
||||
/// A sequential identifier for a stream
|
||||
pub type StreamId = usize;
|
||||
|
||||
/// A sequential identifier for a [`PluginCall`]
|
||||
pub type PluginCallId = usize;
|
||||
|
||||
/// Information about a plugin command invocation. This includes an [`EvaluatedCall`] as a
|
||||
/// serializable representation of [`nu_protocol::ast::Call`]. The type parameter determines
|
||||
/// the input type.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CallInfo<D> {
|
||||
/// The name of the command to be run
|
||||
pub name: String,
|
||||
/// Information about the invocation, including arguments
|
||||
pub call: EvaluatedCall,
|
||||
pub input: CallInput,
|
||||
/// Pipeline input. This is usually [`nu_protocol::PipelineData`] or [`PipelineDataHeader`]
|
||||
pub input: D,
|
||||
/// Plugin configuration, if available
|
||||
pub config: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum CallInput {
|
||||
/// The initial (and perhaps only) part of any [`nu_protocol::PipelineData`] sent over the wire.
|
||||
///
|
||||
/// This may contain a single value, or may initiate a stream with a [`StreamId`].
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub enum PipelineDataHeader {
|
||||
/// No input
|
||||
Empty,
|
||||
/// A single value
|
||||
Value(Value),
|
||||
Data(PluginData),
|
||||
/// Initiate [`nu_protocol::PipelineData::ListStream`].
|
||||
///
|
||||
/// Items are sent via [`StreamData`]
|
||||
ListStream(ListStreamInfo),
|
||||
/// Initiate [`nu_protocol::PipelineData::ExternalStream`].
|
||||
///
|
||||
/// Items are sent via [`StreamData`]
|
||||
ExternalStream(ExternalStreamInfo),
|
||||
}
|
||||
|
||||
// Information sent to the plugin
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum PluginCall {
|
||||
/// Additional information about list (value) streams
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ListStreamInfo {
|
||||
pub id: StreamId,
|
||||
}
|
||||
|
||||
/// Additional information about external streams
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ExternalStreamInfo {
|
||||
pub span: Span,
|
||||
pub stdout: Option<RawStreamInfo>,
|
||||
pub stderr: Option<RawStreamInfo>,
|
||||
pub exit_code: Option<ListStreamInfo>,
|
||||
pub trim_end_newline: bool,
|
||||
}
|
||||
|
||||
/// Additional information about raw (byte) streams
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct RawStreamInfo {
|
||||
pub id: StreamId,
|
||||
pub is_binary: bool,
|
||||
pub known_size: Option<u64>,
|
||||
}
|
||||
|
||||
impl RawStreamInfo {
|
||||
pub(crate) fn new(id: StreamId, stream: &RawStream) -> Self {
|
||||
RawStreamInfo {
|
||||
id,
|
||||
is_binary: stream.is_binary,
|
||||
known_size: stream.known_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls that a plugin can execute. The type parameter determines the input type.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginCall<D> {
|
||||
Signature,
|
||||
CallInfo(CallInfo),
|
||||
CollapseCustomValue(PluginData),
|
||||
Run(CallInfo<D>),
|
||||
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
|
||||
}
|
||||
|
||||
/// Operations supported for custom values.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum CustomValueOp {
|
||||
/// [`to_base_value()`](nu_protocol::CustomValue::to_base_value)
|
||||
ToBaseValue,
|
||||
}
|
||||
|
||||
/// Any data sent to the plugin
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginInput {
|
||||
/// This must be the first message. Indicates supported protocol
|
||||
Hello(ProtocolInfo),
|
||||
/// Execute a [`PluginCall`], such as `Run` or `Signature`. The ID should not have been used
|
||||
/// before.
|
||||
Call(PluginCallId, PluginCall<PipelineDataHeader>),
|
||||
/// Stream control or data message. Untagged to keep them as small as possible.
|
||||
///
|
||||
/// For example, `Stream(Ack(0))` is encoded as `{"Ack": 0}`
|
||||
#[serde(untagged)]
|
||||
Stream(StreamMessage),
|
||||
}
|
||||
|
||||
impl TryFrom<PluginInput> for StreamMessage {
|
||||
type Error = PluginInput;
|
||||
|
||||
fn try_from(msg: PluginInput) -> Result<StreamMessage, PluginInput> {
|
||||
match msg {
|
||||
PluginInput::Stream(stream_msg) => Ok(stream_msg),
|
||||
_ => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for PluginInput {
|
||||
fn from(stream_msg: StreamMessage) -> PluginInput {
|
||||
PluginInput::Stream(stream_msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single item of stream data for a stream.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum StreamData {
|
||||
List(Value),
|
||||
Raw(Result<Vec<u8>, ShellError>),
|
||||
}
|
||||
|
||||
impl From<Value> for StreamData {
|
||||
fn from(value: Value) -> Self {
|
||||
StreamData::List(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Result<Vec<u8>, ShellError>> for StreamData {
|
||||
fn from(value: Result<Vec<u8>, ShellError>) -> Self {
|
||||
StreamData::Raw(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StreamData> for Value {
|
||||
type Error = ShellError;
|
||||
|
||||
fn try_from(data: StreamData) -> Result<Value, ShellError> {
|
||||
match data {
|
||||
StreamData::List(value) => Ok(value),
|
||||
StreamData::Raw(_) => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "expected list stream data, found raw data".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StreamData> for Result<Vec<u8>, ShellError> {
|
||||
type Error = ShellError;
|
||||
|
||||
fn try_from(data: StreamData) -> Result<Result<Vec<u8>, ShellError>, ShellError> {
|
||||
match data {
|
||||
StreamData::Raw(value) => Ok(value),
|
||||
StreamData::List(_) => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "expected raw stream data, found list data".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream control or data message.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum StreamMessage {
|
||||
/// Append data to the stream. Sent by the stream producer.
|
||||
Data(StreamId, StreamData),
|
||||
/// End of stream. Sent by the stream producer.
|
||||
End(StreamId),
|
||||
/// Notify that the read end of the stream has closed, and further messages should not be
|
||||
/// sent. Sent by the stream consumer.
|
||||
Drop(StreamId),
|
||||
/// Acknowledge that a message has been consumed. This is used to implement flow control by
|
||||
/// the stream producer. Sent by the stream consumer.
|
||||
Ack(StreamId),
|
||||
}
|
||||
|
||||
/// An error message with debugging information that can be passed to Nushell from the plugin
|
||||
@@ -36,7 +204,7 @@ pub enum PluginCall {
|
||||
/// a [Plugin](crate::Plugin)'s [`run`](crate::Plugin::run()) method. It contains
|
||||
/// the error message along with optional [Span] data to support highlighting in the
|
||||
/// shell.
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
|
||||
pub struct LabeledError {
|
||||
/// The name of the error
|
||||
pub label: String,
|
||||
@@ -48,81 +216,108 @@ pub struct LabeledError {
|
||||
|
||||
impl From<LabeledError> for ShellError {
|
||||
fn from(error: LabeledError) -> Self {
|
||||
match error.span {
|
||||
Some(span) => ShellError::GenericError {
|
||||
if error.span.is_some() {
|
||||
ShellError::GenericError {
|
||||
error: error.label,
|
||||
msg: error.msg,
|
||||
span: Some(span),
|
||||
span: error.span,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
},
|
||||
None => ShellError::GenericError {
|
||||
}
|
||||
} else {
|
||||
ShellError::GenericError {
|
||||
error: error.label,
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: Some(error.msg),
|
||||
help: (!error.msg.is_empty()).then_some(error.msg),
|
||||
inner: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ShellError> for LabeledError {
|
||||
fn from(error: ShellError) -> Self {
|
||||
match error {
|
||||
ShellError::GenericError {
|
||||
error: label,
|
||||
msg,
|
||||
span,
|
||||
..
|
||||
} => LabeledError { label, msg, span },
|
||||
ShellError::CantConvert {
|
||||
to_type: expected,
|
||||
from_type: input,
|
||||
span,
|
||||
help: _help,
|
||||
} => LabeledError {
|
||||
label: format!("Can't convert to {expected}"),
|
||||
msg: format!("can't convert from {input} to {expected}"),
|
||||
use miette::Diagnostic;
|
||||
// This is not perfect - we can only take the first labeled span as that's all we have
|
||||
// space for.
|
||||
if let Some(labeled_span) = error.labels().and_then(|mut iter| iter.nth(0)) {
|
||||
let offset = labeled_span.offset();
|
||||
let span = Span::new(offset, offset + labeled_span.len());
|
||||
LabeledError {
|
||||
label: error.to_string(),
|
||||
msg: labeled_span
|
||||
.label()
|
||||
.map(|label| label.to_owned())
|
||||
.unwrap_or_else(|| "".into()),
|
||||
span: Some(span),
|
||||
},
|
||||
ShellError::DidYouMean { suggestion, span } => LabeledError {
|
||||
label: "Name not found".into(),
|
||||
msg: format!("did you mean '{suggestion}'?"),
|
||||
span: Some(span),
|
||||
},
|
||||
ShellError::PluginFailedToLoad { msg } => LabeledError {
|
||||
label: "Plugin failed to load".into(),
|
||||
msg,
|
||||
}
|
||||
} else {
|
||||
LabeledError {
|
||||
label: error.to_string(),
|
||||
msg: error
|
||||
.help()
|
||||
.map(|help| help.to_string())
|
||||
.unwrap_or_else(|| "".into()),
|
||||
span: None,
|
||||
},
|
||||
ShellError::PluginFailedToEncode { msg } => LabeledError {
|
||||
label: "Plugin failed to encode".into(),
|
||||
msg,
|
||||
span: None,
|
||||
},
|
||||
ShellError::PluginFailedToDecode { msg } => LabeledError {
|
||||
label: "Plugin failed to decode".into(),
|
||||
msg,
|
||||
span: None,
|
||||
},
|
||||
err => LabeledError {
|
||||
label: "Error - Add to LabeledError From<ShellError>".into(),
|
||||
msg: err.to_string(),
|
||||
span: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Information received from the plugin
|
||||
// Needs to be public to communicate with nu-parser but not typically
|
||||
// used by Plugin authors
|
||||
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
|
||||
///
|
||||
/// Note: exported for internal use, not public.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum PluginResponse {
|
||||
pub enum PluginCallResponse<D> {
|
||||
Error(LabeledError),
|
||||
Signature(Vec<PluginSignature>),
|
||||
Value(Box<Value>),
|
||||
PluginData(String, PluginData),
|
||||
PipelineData(D),
|
||||
}
|
||||
|
||||
impl PluginCallResponse<PipelineDataHeader> {
|
||||
/// Construct a plugin call response with a single value
|
||||
pub fn value(value: Value) -> PluginCallResponse<PipelineDataHeader> {
|
||||
if value.is_nothing() {
|
||||
PluginCallResponse::PipelineData(PipelineDataHeader::Empty)
|
||||
} else {
|
||||
PluginCallResponse::PipelineData(PipelineDataHeader::Value(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information received from the plugin
|
||||
///
|
||||
/// Note: exported for internal use, not public.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
pub enum PluginOutput {
|
||||
/// This must be the first message. Indicates supported protocol
|
||||
Hello(ProtocolInfo),
|
||||
/// A response to a [`PluginCall`]. The ID should be the same sent with the plugin call this
|
||||
/// is a response to
|
||||
CallResponse(PluginCallId, PluginCallResponse<PipelineDataHeader>),
|
||||
/// Stream control or data message. Untagged to keep them as small as possible.
|
||||
///
|
||||
/// For example, `Stream(Ack(0))` is encoded as `{"Ack": 0}`
|
||||
#[serde(untagged)]
|
||||
Stream(StreamMessage),
|
||||
}
|
||||
|
||||
impl TryFrom<PluginOutput> for StreamMessage {
|
||||
type Error = PluginOutput;
|
||||
|
||||
fn try_from(msg: PluginOutput) -> Result<StreamMessage, PluginOutput> {
|
||||
match msg {
|
||||
PluginOutput::Stream(stream_msg) => Ok(stream_msg),
|
||||
_ => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for PluginOutput {
|
||||
fn from(stream_msg: StreamMessage) -> PluginOutput {
|
||||
PluginOutput::Stream(stream_msg)
|
||||
}
|
||||
}
|
||||
|
@@ -1,37 +1,39 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_protocol::{CustomValue, ShellError, Value};
|
||||
use serde::Serialize;
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Spanned, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::plugin::{call_plugin, create_command, get_plugin_encoding};
|
||||
use crate::plugin::PluginIdentity;
|
||||
|
||||
use super::{PluginCall, PluginData, PluginResponse};
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// An opaque container for a custom value that is handled fully by a plugin
|
||||
///
|
||||
/// This is constructed by the main nushell engine when it receives [`PluginResponse::PluginData`]
|
||||
/// it stores that data as well as metadata related to the plugin to be able to call the plugin
|
||||
/// later.
|
||||
/// Since the data in it is opaque to the engine, there are only two final destinations for it:
|
||||
/// either it will be sent back to the plugin that generated it across a pipeline, or it will be
|
||||
/// sent to the plugin with a request to collapse it into a base value
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
/// This is the only type of custom value that is allowed to cross the plugin serialization
|
||||
/// boundary.
|
||||
///
|
||||
/// [`EngineInterface`](crate::interface::EngineInterface) is responsible for ensuring
|
||||
/// that local plugin custom values are converted to and from [`PluginCustomData`] on the boundary.
|
||||
///
|
||||
/// [`PluginInterface`](crate::interface::PluginInterface) is responsible for adding the
|
||||
/// appropriate [`PluginIdentity`](crate::plugin::PluginIdentity), ensuring that only
|
||||
/// [`PluginCustomData`] is contained within any values sent, and that the `source` of any
|
||||
/// values sent matches the plugin it is being sent to.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluginCustomValue {
|
||||
/// The name of the custom value as defined by the plugin
|
||||
/// The name of the custom value as defined by the plugin (`value_string()`)
|
||||
pub name: String,
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
pub data: Vec<u8>,
|
||||
pub filename: PathBuf,
|
||||
|
||||
// PluginCustomValue must implement Serialize because all CustomValues must implement Serialize
|
||||
// However, the main place where values are serialized and deserialized is when they are being
|
||||
// sent between plugins and nushell's main engine. PluginCustomValue is never meant to be sent
|
||||
// between that boundary
|
||||
#[serde(skip)]
|
||||
pub shell: Option<PathBuf>,
|
||||
#[serde(skip)]
|
||||
pub source: String,
|
||||
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
|
||||
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
|
||||
#[serde(skip, default)]
|
||||
pub source: Option<Arc<PluginIdentity>>,
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for PluginCustomValue {
|
||||
fn clone_value(&self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
Value::custom_value(Box::new(self.clone()), span)
|
||||
@@ -45,83 +47,295 @@ impl CustomValue for PluginCustomValue {
|
||||
&self,
|
||||
span: nu_protocol::Span,
|
||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||
let mut plugin_cmd = create_command(&self.filename, self.shell.as_deref());
|
||||
|
||||
let mut child = plugin_cmd.spawn().map_err(|err| ShellError::GenericError {
|
||||
let wrap_err = |err: ShellError| ShellError::GenericError {
|
||||
error: format!(
|
||||
"Unable to spawn plugin for {} to get base value",
|
||||
"Unable to spawn plugin `{}` to get base value",
|
||||
self.source
|
||||
.as_ref()
|
||||
.map(|s| s.plugin_name.as_str())
|
||||
.unwrap_or("<unknown>")
|
||||
),
|
||||
msg: format!("{err}"),
|
||||
msg: err.to_string(),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
inner: vec![err],
|
||||
};
|
||||
|
||||
let identity = self.source.clone().ok_or_else(|| {
|
||||
wrap_err(ShellError::NushellFailed {
|
||||
msg: "The plugin source for the custom value was not set".into(),
|
||||
})
|
||||
})?;
|
||||
|
||||
let plugin_call = PluginCall::CollapseCustomValue(PluginData {
|
||||
data: self.data.clone(),
|
||||
span,
|
||||
});
|
||||
let encoding = {
|
||||
let stdout_reader = match &mut child.stdout {
|
||||
Some(out) => out,
|
||||
None => {
|
||||
return Err(ShellError::PluginFailedToLoad {
|
||||
msg: "Plugin missing stdout reader".into(),
|
||||
})
|
||||
}
|
||||
};
|
||||
get_plugin_encoding(stdout_reader)?
|
||||
};
|
||||
let empty_env: Option<(String, String)> = None;
|
||||
let plugin = identity.spawn(empty_env).map_err(wrap_err)?;
|
||||
|
||||
let response = call_plugin(&mut child, plugin_call, &encoding, span).map_err(|err| {
|
||||
ShellError::GenericError {
|
||||
error: format!(
|
||||
"Unable to decode call for {} to get base value",
|
||||
self.source
|
||||
),
|
||||
msg: format!("{err}"),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}
|
||||
});
|
||||
|
||||
let value = match response {
|
||||
Ok(PluginResponse::Value(value)) => Ok(*value),
|
||||
Ok(PluginResponse::PluginData(..)) => Err(ShellError::GenericError {
|
||||
error: "Plugin misbehaving".into(),
|
||||
msg: "Plugin returned custom data as a response to a collapse call".into(),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}),
|
||||
Ok(PluginResponse::Error(err)) => Err(err.into()),
|
||||
Ok(PluginResponse::Signature(..)) => Err(ShellError::GenericError {
|
||||
error: "Plugin missing value".into(),
|
||||
msg: "Received a signature from plugin instead of value".into(),
|
||||
span: Some(span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
// We need to call .wait() on the child, or we'll risk summoning the zombie horde
|
||||
let _ = child.wait();
|
||||
|
||||
value
|
||||
plugin
|
||||
.custom_value_to_base_value(Spanned {
|
||||
item: self.clone(),
|
||||
span,
|
||||
})
|
||||
.map_err(wrap_err)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
"PluginCustomValue"
|
||||
impl PluginCustomValue {
|
||||
/// Serialize a custom value into a [`PluginCustomValue`]. This should only be done on the
|
||||
/// plugin side.
|
||||
pub(crate) fn serialize_from_custom_value(
|
||||
custom_value: &dyn CustomValue,
|
||||
span: Span,
|
||||
) -> Result<PluginCustomValue, ShellError> {
|
||||
let name = custom_value.value_string();
|
||||
bincode::serialize(custom_value)
|
||||
.map(|data| PluginCustomValue {
|
||||
name,
|
||||
data,
|
||||
source: None,
|
||||
})
|
||||
.map_err(|err| ShellError::CustomValueFailedToEncode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn typetag_deserialize(&self) {
|
||||
unimplemented!("typetag_deserialize")
|
||||
/// Deserialize a [`PluginCustomValue`] into a `Box<dyn CustomValue>`. This should only be done
|
||||
/// on the plugin side.
|
||||
pub(crate) fn deserialize_to_custom_value(
|
||||
&self,
|
||||
span: Span,
|
||||
) -> Result<Box<dyn CustomValue>, ShellError> {
|
||||
bincode::deserialize::<Box<dyn CustomValue>>(&self.data).map_err(|err| {
|
||||
ShellError::CustomValueFailedToDecode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a [`PluginIdentity`] to all [`PluginCustomValue`]s within a value, recursively.
|
||||
pub(crate) fn add_source(value: &mut Value, source: &Arc<PluginIdentity>) {
|
||||
let span = value.span();
|
||||
match value {
|
||||
// Set source on custom value
|
||||
Value::CustomValue { ref val, .. } => {
|
||||
if let Some(custom_value) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
// Since there's no `as_mut_any()`, we have to copy the whole thing
|
||||
let mut custom_value = custom_value.clone();
|
||||
custom_value.source = Some(source.clone());
|
||||
*value = Value::custom_value(Box::new(custom_value), span);
|
||||
}
|
||||
}
|
||||
// Any values that can contain other values need to be handled recursively
|
||||
Value::Range { ref mut val, .. } => {
|
||||
Self::add_source(&mut val.from, source);
|
||||
Self::add_source(&mut val.to, source);
|
||||
Self::add_source(&mut val.incr, source);
|
||||
}
|
||||
Value::Record { ref mut val, .. } => {
|
||||
for (_, rec_value) in val.iter_mut() {
|
||||
Self::add_source(rec_value, source);
|
||||
}
|
||||
}
|
||||
Value::List { ref mut vals, .. } => {
|
||||
for list_value in vals.iter_mut() {
|
||||
Self::add_source(list_value, source);
|
||||
}
|
||||
}
|
||||
// All of these don't contain other values
|
||||
Value::Bool { .. }
|
||||
| Value::Int { .. }
|
||||
| Value::Float { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::Duration { .. }
|
||||
| Value::Date { .. }
|
||||
| Value::String { .. }
|
||||
| Value::Glob { .. }
|
||||
| Value::Block { .. }
|
||||
| Value::Closure { .. }
|
||||
| Value::Nothing { .. }
|
||||
| Value::Error { .. }
|
||||
| Value::Binary { .. }
|
||||
| Value::CellPath { .. } => (),
|
||||
// LazyRecord could generate other values, but we shouldn't be receiving it anyway
|
||||
//
|
||||
// It's better to handle this as a bug
|
||||
Value::LazyRecord { .. } => unimplemented!("add_source for LazyRecord"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that all [`CustomValue`]s present within the `value` are [`PluginCustomValue`]s that
|
||||
/// come from the given `source`, and return an error if not.
|
||||
///
|
||||
/// This method will collapse `LazyRecord` in-place as necessary to make the guarantee,
|
||||
/// since `LazyRecord` could return something different the next time it is called.
|
||||
pub(crate) fn verify_source(
|
||||
value: &mut Value,
|
||||
source: &PluginIdentity,
|
||||
) -> Result<(), ShellError> {
|
||||
let span = value.span();
|
||||
match value {
|
||||
// Set source on custom value
|
||||
Value::CustomValue { val, .. } => {
|
||||
if let Some(custom_value) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
if custom_value.source.as_deref() == Some(source) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: custom_value.name.clone(),
|
||||
span,
|
||||
dest_plugin: source.plugin_name.clone(),
|
||||
src_plugin: custom_value.source.as_ref().map(|s| s.plugin_name.clone()),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Only PluginCustomValues can be sent
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: val.value_string(),
|
||||
span,
|
||||
dest_plugin: source.plugin_name.clone(),
|
||||
src_plugin: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Any values that can contain other values need to be handled recursively
|
||||
Value::Range { val, .. } => {
|
||||
Self::verify_source(&mut val.from, source)?;
|
||||
Self::verify_source(&mut val.to, source)?;
|
||||
Self::verify_source(&mut val.incr, source)
|
||||
}
|
||||
Value::Record { ref mut val, .. } => val
|
||||
.iter_mut()
|
||||
.try_for_each(|(_, rec_value)| Self::verify_source(rec_value, source)),
|
||||
Value::List { ref mut vals, .. } => vals
|
||||
.iter_mut()
|
||||
.try_for_each(|list_value| Self::verify_source(list_value, source)),
|
||||
// All of these don't contain other values
|
||||
Value::Bool { .. }
|
||||
| Value::Int { .. }
|
||||
| Value::Float { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::Duration { .. }
|
||||
| Value::Date { .. }
|
||||
| Value::String { .. }
|
||||
| Value::Glob { .. }
|
||||
| Value::Block { .. }
|
||||
| Value::Closure { .. }
|
||||
| Value::Nothing { .. }
|
||||
| Value::Error { .. }
|
||||
| Value::Binary { .. }
|
||||
| Value::CellPath { .. } => Ok(()),
|
||||
// LazyRecord would be a problem for us, since it could return something else the next
|
||||
// time, and we have to collect it anyway to serialize it. Collect it in place, and then
|
||||
// verify the source of the result
|
||||
Value::LazyRecord { val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Self::verify_source(value, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub(crate) fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::CustomValue { ref val, .. } => {
|
||||
if val.as_any().downcast_ref::<PluginCustomValue>().is_some() {
|
||||
// Already a PluginCustomValue
|
||||
Ok(())
|
||||
} else {
|
||||
let serialized = Self::serialize_from_custom_value(&**val, span)?;
|
||||
*value = Value::custom_value(Box::new(serialized), span);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Any values that can contain other values need to be handled recursively
|
||||
Value::Range { ref mut val, .. } => {
|
||||
Self::serialize_custom_values_in(&mut val.from)?;
|
||||
Self::serialize_custom_values_in(&mut val.to)?;
|
||||
Self::serialize_custom_values_in(&mut val.incr)
|
||||
}
|
||||
Value::Record { ref mut val, .. } => val
|
||||
.iter_mut()
|
||||
.try_for_each(|(_, rec_value)| Self::serialize_custom_values_in(rec_value)),
|
||||
Value::List { ref mut vals, .. } => vals
|
||||
.iter_mut()
|
||||
.try_for_each(Self::serialize_custom_values_in),
|
||||
// All of these don't contain other values
|
||||
Value::Bool { .. }
|
||||
| Value::Int { .. }
|
||||
| Value::Float { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::Duration { .. }
|
||||
| Value::Date { .. }
|
||||
| Value::String { .. }
|
||||
| Value::Glob { .. }
|
||||
| Value::Block { .. }
|
||||
| Value::Closure { .. }
|
||||
| Value::Nothing { .. }
|
||||
| Value::Error { .. }
|
||||
| Value::Binary { .. }
|
||||
| Value::CellPath { .. } => Ok(()),
|
||||
// Collect any lazy records that exist and try again
|
||||
Value::LazyRecord { val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Self::serialize_custom_values_in(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub(crate) fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::CustomValue { ref val, .. } => {
|
||||
if let Some(val) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
let deserialized = val.deserialize_to_custom_value(span)?;
|
||||
*value = Value::custom_value(deserialized, span);
|
||||
Ok(())
|
||||
} else {
|
||||
// Already not a PluginCustomValue
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Any values that can contain other values need to be handled recursively
|
||||
Value::Range { ref mut val, .. } => {
|
||||
Self::deserialize_custom_values_in(&mut val.from)?;
|
||||
Self::deserialize_custom_values_in(&mut val.to)?;
|
||||
Self::deserialize_custom_values_in(&mut val.incr)
|
||||
}
|
||||
Value::Record { ref mut val, .. } => val
|
||||
.iter_mut()
|
||||
.try_for_each(|(_, rec_value)| Self::deserialize_custom_values_in(rec_value)),
|
||||
Value::List { ref mut vals, .. } => vals
|
||||
.iter_mut()
|
||||
.try_for_each(Self::deserialize_custom_values_in),
|
||||
// All of these don't contain other values
|
||||
Value::Bool { .. }
|
||||
| Value::Int { .. }
|
||||
| Value::Float { .. }
|
||||
| Value::Filesize { .. }
|
||||
| Value::Duration { .. }
|
||||
| Value::Date { .. }
|
||||
| Value::String { .. }
|
||||
| Value::Glob { .. }
|
||||
| Value::Block { .. }
|
||||
| Value::Closure { .. }
|
||||
| Value::Nothing { .. }
|
||||
| Value::Error { .. }
|
||||
| Value::Binary { .. }
|
||||
| Value::CellPath { .. } => Ok(()),
|
||||
// Collect any lazy records that exist and try again
|
||||
Value::LazyRecord { val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Self::deserialize_custom_values_in(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
492
crates/nu-plugin/src/protocol/plugin_custom_value/tests.rs
Normal file
492
crates/nu-plugin/src/protocol/plugin_custom_value/tests.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
use nu_protocol::{ast::RangeInclusion, record, CustomValue, Range, ShellError, Span, Value};
|
||||
|
||||
use crate::{
|
||||
plugin::PluginIdentity,
|
||||
protocol::test_util::{
|
||||
expected_test_custom_value, test_plugin_custom_value, test_plugin_custom_value_with_source,
|
||||
TestCustomValue,
|
||||
},
|
||||
};
|
||||
|
||||
use super::PluginCustomValue;
|
||||
|
||||
#[test]
|
||||
fn serialize_deserialize() -> Result<(), ShellError> {
|
||||
let original_value = TestCustomValue(32);
|
||||
let span = Span::test_data();
|
||||
let serialized = PluginCustomValue::serialize_from_custom_value(&original_value, span)?;
|
||||
assert_eq!(original_value.value_string(), serialized.name);
|
||||
assert!(serialized.source.is_none());
|
||||
let deserialized = serialized.deserialize_to_custom_value(span)?;
|
||||
let downcasted = deserialized
|
||||
.as_any()
|
||||
.downcast_ref::<TestCustomValue>()
|
||||
.expect("failed to downcast: not TestCustomValue");
|
||||
assert_eq!(original_value, *downcasted);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_serialize_output() -> Result<(), ShellError> {
|
||||
let original_value = expected_test_custom_value();
|
||||
let span = Span::test_data();
|
||||
let serialized = PluginCustomValue::serialize_from_custom_value(&original_value, span)?;
|
||||
assert_eq!(
|
||||
test_plugin_custom_value().data,
|
||||
serialized.data,
|
||||
"The bincode configuration is probably different from what we expected. \
|
||||
Fix test_plugin_custom_value() to match it"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_at_root() -> Result<(), ShellError> {
|
||||
let mut val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let source = PluginIdentity::new_fake("foo");
|
||||
PluginCustomValue::add_source(&mut val, &source);
|
||||
|
||||
let custom_value = val.as_custom_value()?;
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("not PluginCustomValue");
|
||||
assert_eq!(Some(source), plugin_custom_value.source);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_range_custom_values(
|
||||
val: &Value,
|
||||
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let range = val.as_range()?;
|
||||
for (name, val) in [
|
||||
("from", &range.from),
|
||||
("incr", &range.incr),
|
||||
("to", &range.to),
|
||||
] {
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("{name} not custom value"));
|
||||
f(name, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_nested_range() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_range(Range {
|
||||
from: orig_custom_val.clone(),
|
||||
incr: orig_custom_val.clone(),
|
||||
to: orig_custom_val.clone(),
|
||||
inclusion: RangeInclusion::Inclusive,
|
||||
});
|
||||
let source = PluginIdentity::new_fake("foo");
|
||||
PluginCustomValue::add_source(&mut val, &source);
|
||||
|
||||
check_range_custom_values(&val, |name, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("{name} not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(&source),
|
||||
plugin_custom_value.source.as_ref(),
|
||||
"{name} source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_record_custom_values(
|
||||
val: &Value,
|
||||
keys: &[&str],
|
||||
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let record = val.as_record()?;
|
||||
for key in keys {
|
||||
let val = record
|
||||
.get(key)
|
||||
.unwrap_or_else(|| panic!("record does not contain '{key}'"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("'{key}' not custom value"));
|
||||
f(key, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_nested_record() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_record(record! {
|
||||
"foo" => orig_custom_val.clone(),
|
||||
"bar" => orig_custom_val.clone(),
|
||||
});
|
||||
let source = PluginIdentity::new_fake("foo");
|
||||
PluginCustomValue::add_source(&mut val, &source);
|
||||
|
||||
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(&source),
|
||||
plugin_custom_value.source.as_ref(),
|
||||
"'{key}' source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_list_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let list = val.as_list()?;
|
||||
for index in indices {
|
||||
let val = list
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in list"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_nested_list() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
|
||||
let source = PluginIdentity::new_fake("foo");
|
||||
PluginCustomValue::add_source(&mut val, &source);
|
||||
|
||||
check_list_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(&source),
|
||||
plugin_custom_value.source.as_ref(),
|
||||
"[{index}] source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_error_message() -> Result<(), ShellError> {
|
||||
let span = Span::new(5, 7);
|
||||
let mut ok_val = Value::custom_value(Box::new(test_plugin_custom_value_with_source()), span);
|
||||
let mut native_val = Value::custom_value(Box::new(TestCustomValue(32)), span);
|
||||
let mut foreign_val = {
|
||||
let mut val = test_plugin_custom_value();
|
||||
val.source = Some(PluginIdentity::new_fake("other"));
|
||||
Value::custom_value(Box::new(val), span)
|
||||
};
|
||||
let source = PluginIdentity::new_fake("test");
|
||||
|
||||
PluginCustomValue::verify_source(&mut ok_val, &source).expect("ok_val should be verified ok");
|
||||
|
||||
for (val, src_plugin) in [(&mut native_val, None), (&mut foreign_val, Some("other"))] {
|
||||
let error = PluginCustomValue::verify_source(val, &source).expect_err(&format!(
|
||||
"a custom value from {src_plugin:?} should result in an error"
|
||||
));
|
||||
if let ShellError::CustomValueIncorrectForPlugin {
|
||||
name,
|
||||
span: err_span,
|
||||
dest_plugin,
|
||||
src_plugin: err_src_plugin,
|
||||
} = error
|
||||
{
|
||||
assert_eq!("TestCustomValue", name, "error.name from {src_plugin:?}");
|
||||
assert_eq!(span, err_span, "error.span from {src_plugin:?}");
|
||||
assert_eq!("test", dest_plugin, "error.dest_plugin from {src_plugin:?}");
|
||||
assert_eq!(src_plugin, err_src_plugin.as_deref(), "error.src_plugin");
|
||||
} else {
|
||||
panic!("the error returned should be CustomValueIncorrectForPlugin");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_nested_range() -> Result<(), ShellError> {
|
||||
let native_val = Value::test_custom_value(Box::new(TestCustomValue(32)));
|
||||
let source = PluginIdentity::new_fake("test");
|
||||
for (name, mut val) in [
|
||||
(
|
||||
"from",
|
||||
Value::test_range(Range {
|
||||
from: native_val.clone(),
|
||||
incr: Value::test_nothing(),
|
||||
to: Value::test_nothing(),
|
||||
inclusion: RangeInclusion::RightExclusive,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"incr",
|
||||
Value::test_range(Range {
|
||||
from: Value::test_nothing(),
|
||||
incr: native_val.clone(),
|
||||
to: Value::test_nothing(),
|
||||
inclusion: RangeInclusion::RightExclusive,
|
||||
}),
|
||||
),
|
||||
(
|
||||
"to",
|
||||
Value::test_range(Range {
|
||||
from: Value::test_nothing(),
|
||||
incr: Value::test_nothing(),
|
||||
to: native_val.clone(),
|
||||
inclusion: RangeInclusion::RightExclusive,
|
||||
}),
|
||||
),
|
||||
] {
|
||||
PluginCustomValue::verify_source(&mut val, &source)
|
||||
.expect_err(&format!("error not generated on {name}"));
|
||||
}
|
||||
|
||||
let mut ok_range = Value::test_range(Range {
|
||||
from: Value::test_nothing(),
|
||||
incr: Value::test_nothing(),
|
||||
to: Value::test_nothing(),
|
||||
inclusion: RangeInclusion::RightExclusive,
|
||||
});
|
||||
PluginCustomValue::verify_source(&mut ok_range, &source)
|
||||
.expect("ok_range should not generate error");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_nested_record() -> Result<(), ShellError> {
|
||||
let native_val = Value::test_custom_value(Box::new(TestCustomValue(32)));
|
||||
let source = PluginIdentity::new_fake("test");
|
||||
for (name, mut val) in [
|
||||
(
|
||||
"first element foo",
|
||||
Value::test_record(record! {
|
||||
"foo" => native_val.clone(),
|
||||
"bar" => Value::test_nothing(),
|
||||
}),
|
||||
),
|
||||
(
|
||||
"second element bar",
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_nothing(),
|
||||
"bar" => native_val.clone(),
|
||||
}),
|
||||
),
|
||||
] {
|
||||
PluginCustomValue::verify_source(&mut val, &source)
|
||||
.expect_err(&format!("error not generated on {name}"));
|
||||
}
|
||||
|
||||
let mut ok_record = Value::test_record(record! {"foo" => Value::test_nothing()});
|
||||
PluginCustomValue::verify_source(&mut ok_record, &source)
|
||||
.expect("ok_record should not generate error");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_nested_list() -> Result<(), ShellError> {
|
||||
let native_val = Value::test_custom_value(Box::new(TestCustomValue(32)));
|
||||
let source = PluginIdentity::new_fake("test");
|
||||
for (name, mut val) in [
|
||||
(
|
||||
"first element",
|
||||
Value::test_list(vec![native_val.clone(), Value::test_nothing()]),
|
||||
),
|
||||
(
|
||||
"second element",
|
||||
Value::test_list(vec![Value::test_nothing(), native_val.clone()]),
|
||||
),
|
||||
] {
|
||||
PluginCustomValue::verify_source(&mut val, &source)
|
||||
.expect_err(&format!("error not generated on {name}"));
|
||||
}
|
||||
|
||||
let mut ok_list = Value::test_list(vec![Value::test_nothing()]);
|
||||
PluginCustomValue::verify_source(&mut ok_list, &source)
|
||||
.expect("ok_list should not generate error");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_in_root() -> Result<(), ShellError> {
|
||||
let span = Span::new(4, 10);
|
||||
let mut val = Value::custom_value(Box::new(expected_test_custom_value()), span);
|
||||
PluginCustomValue::serialize_custom_values_in(&mut val)?;
|
||||
|
||||
assert_eq!(span, val.span());
|
||||
|
||||
let custom_value = val.as_custom_value()?;
|
||||
if let Some(plugin_custom_value) = custom_value.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
assert_eq!("TestCustomValue", plugin_custom_value.name);
|
||||
assert_eq!(test_plugin_custom_value().data, plugin_custom_value.data);
|
||||
assert!(plugin_custom_value.source.is_none());
|
||||
} else {
|
||||
panic!("Failed to downcast to PluginCustomValue");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_in_range() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(-1)));
|
||||
let mut val = Value::test_range(Range {
|
||||
from: orig_custom_val.clone(),
|
||||
incr: orig_custom_val.clone(),
|
||||
to: orig_custom_val.clone(),
|
||||
inclusion: RangeInclusion::Inclusive,
|
||||
});
|
||||
PluginCustomValue::serialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_range_custom_values(&val, |name, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("{name} not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
"TestCustomValue", plugin_custom_value.name,
|
||||
"{name} name not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_in_record() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(32)));
|
||||
let mut val = Value::test_record(record! {
|
||||
"foo" => orig_custom_val.clone(),
|
||||
"bar" => orig_custom_val.clone(),
|
||||
});
|
||||
PluginCustomValue::serialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
"TestCustomValue", plugin_custom_value.name,
|
||||
"'{key}' name not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_in_list() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(24)));
|
||||
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
|
||||
PluginCustomValue::serialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_list_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
"TestCustomValue", plugin_custom_value.name,
|
||||
"[{index}] name not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_in_root() -> Result<(), ShellError> {
|
||||
let span = Span::new(4, 10);
|
||||
let mut val = Value::custom_value(Box::new(test_plugin_custom_value()), span);
|
||||
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
|
||||
|
||||
assert_eq!(span, val.span());
|
||||
|
||||
let custom_value = val.as_custom_value()?;
|
||||
if let Some(test_custom_value) = custom_value.as_any().downcast_ref::<TestCustomValue>() {
|
||||
assert_eq!(expected_test_custom_value(), *test_custom_value);
|
||||
} else {
|
||||
panic!("Failed to downcast to TestCustomValue");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_in_range() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_range(Range {
|
||||
from: orig_custom_val.clone(),
|
||||
incr: orig_custom_val.clone(),
|
||||
to: orig_custom_val.clone(),
|
||||
inclusion: RangeInclusion::Inclusive,
|
||||
});
|
||||
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_range_custom_values(&val, |name, custom_value| {
|
||||
let test_custom_value: &TestCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("{name} not TestCustomValue"));
|
||||
assert_eq!(
|
||||
expected_test_custom_value(),
|
||||
*test_custom_value,
|
||||
"{name} not deserialized correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_in_record() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_record(record! {
|
||||
"foo" => orig_custom_val.clone(),
|
||||
"bar" => orig_custom_val.clone(),
|
||||
});
|
||||
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
|
||||
let test_custom_value: &TestCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("'{key}' not TestCustomValue"));
|
||||
assert_eq!(
|
||||
expected_test_custom_value(),
|
||||
*test_custom_value,
|
||||
"{key} not deserialized correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_in_list() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
|
||||
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_list_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let test_custom_value: &TestCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not TestCustomValue"));
|
||||
assert_eq!(
|
||||
expected_test_custom_value(),
|
||||
*test_custom_value,
|
||||
"[{index}] name not deserialized correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
@@ -1,8 +0,0 @@
|
||||
use nu_protocol::Span;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct PluginData {
|
||||
pub data: Vec<u8>,
|
||||
pub span: Span,
|
||||
}
|
80
crates/nu-plugin/src/protocol/protocol_info.rs
Normal file
80
crates/nu-plugin/src/protocol/protocol_info.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use nu_protocol::ShellError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Protocol information, sent as a `Hello` message on initialization. This determines the
|
||||
/// compatibility of the plugin and engine. They are considered to be compatible if the lower
|
||||
/// version is semver compatible with the higher one.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ProtocolInfo {
|
||||
/// The name of the protocol being implemented. Only one protocol is supported. This field
|
||||
/// can be safely ignored, because not matching is a deserialization error
|
||||
pub protocol: Protocol,
|
||||
/// The semantic version of the protocol. This should be the version of the `nu-plugin`
|
||||
/// crate
|
||||
pub version: String,
|
||||
/// Supported optional features. This helps to maintain semver compatibility when adding new
|
||||
/// features
|
||||
pub features: Vec<Feature>,
|
||||
}
|
||||
|
||||
impl Default for ProtocolInfo {
|
||||
fn default() -> ProtocolInfo {
|
||||
ProtocolInfo {
|
||||
protocol: Protocol::NuPlugin,
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
features: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolInfo {
|
||||
pub fn is_compatible_with(&self, other: &ProtocolInfo) -> Result<bool, ShellError> {
|
||||
fn parse_failed(error: semver::Error) -> ShellError {
|
||||
ShellError::PluginFailedToLoad {
|
||||
msg: format!("Failed to parse protocol version: {error}"),
|
||||
}
|
||||
}
|
||||
let mut versions = [
|
||||
semver::Version::parse(&self.version).map_err(parse_failed)?,
|
||||
semver::Version::parse(&other.version).map_err(parse_failed)?,
|
||||
];
|
||||
|
||||
versions.sort();
|
||||
|
||||
// For example, if the lower version is 1.1.0, and the higher version is 1.2.3, the
|
||||
// requirement is that 1.2.3 matches ^1.1.0 (which it does)
|
||||
Ok(semver::Comparator {
|
||||
op: semver::Op::Caret,
|
||||
major: versions[0].major,
|
||||
minor: Some(versions[0].minor),
|
||||
patch: Some(versions[0].patch),
|
||||
pre: versions[0].pre.clone(),
|
||||
}
|
||||
.matches(&versions[1]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates the protocol in use. Only one protocol is supported.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||
pub enum Protocol {
|
||||
/// Serializes to the value `"nu-plugin"`
|
||||
#[serde(rename = "nu-plugin")]
|
||||
#[default]
|
||||
NuPlugin,
|
||||
}
|
||||
|
||||
/// Indicates optional protocol features. This can help to make non-breaking-change additions to
|
||||
/// the protocol. Features are not restricted to plain strings and can contain additional
|
||||
/// configuration data.
|
||||
///
|
||||
/// Optional features should not be used by the protocol if they are not present in the
|
||||
/// [`ProtocolInfo`] sent by the other side.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "name")]
|
||||
pub enum Feature {
|
||||
/// A feature that was not recognized on deserialization. Attempting to serialize this feature
|
||||
/// is an error. Matching against it may only be used if necessary to determine whether
|
||||
/// unsupported features are present.
|
||||
#[serde(other, skip_serializing)]
|
||||
Unknown,
|
||||
}
|
50
crates/nu-plugin/src/protocol/test_util.rs
Normal file
50
crates/nu-plugin/src/protocol/test_util.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::plugin::PluginIdentity;
|
||||
|
||||
use super::PluginCustomValue;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestCustomValue(pub i32);
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for TestCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
Value::custom_value(Box::new(self.clone()), span)
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
"TestCustomValue".into()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
Ok(Value::int(self.0 as i64, span))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn test_plugin_custom_value() -> PluginCustomValue {
|
||||
let data = bincode::serialize(&expected_test_custom_value() as &dyn CustomValue)
|
||||
.expect("bincode serialization of the expected_test_custom_value() failed");
|
||||
|
||||
PluginCustomValue {
|
||||
name: "TestCustomValue".into(),
|
||||
data,
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expected_test_custom_value() -> TestCustomValue {
|
||||
TestCustomValue(-1)
|
||||
}
|
||||
|
||||
pub(crate) fn test_plugin_custom_value_with_source() -> PluginCustomValue {
|
||||
PluginCustomValue {
|
||||
source: Some(PluginIdentity::new_fake("test")),
|
||||
..test_plugin_custom_value()
|
||||
}
|
||||
}
|
35
crates/nu-plugin/src/protocol/tests.rs
Normal file
35
crates/nu-plugin/src/protocol/tests.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn protocol_info_compatible() -> Result<(), ShellError> {
|
||||
let ver_1_2_3 = ProtocolInfo {
|
||||
protocol: Protocol::NuPlugin,
|
||||
version: "1.2.3".into(),
|
||||
features: vec![],
|
||||
};
|
||||
let ver_1_1_0 = ProtocolInfo {
|
||||
protocol: Protocol::NuPlugin,
|
||||
version: "1.1.0".into(),
|
||||
features: vec![],
|
||||
};
|
||||
assert!(ver_1_1_0.is_compatible_with(&ver_1_2_3)?);
|
||||
assert!(ver_1_2_3.is_compatible_with(&ver_1_1_0)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_info_incompatible() -> Result<(), ShellError> {
|
||||
let ver_2_0_0 = ProtocolInfo {
|
||||
protocol: Protocol::NuPlugin,
|
||||
version: "2.0.0".into(),
|
||||
features: vec![],
|
||||
};
|
||||
let ver_1_1_0 = ProtocolInfo {
|
||||
protocol: Protocol::NuPlugin,
|
||||
version: "1.1.0".into(),
|
||||
features: vec![],
|
||||
};
|
||||
assert!(!ver_2_0_0.is_compatible_with(&ver_1_1_0)?);
|
||||
assert!(!ver_1_1_0.is_compatible_with(&ver_2_0_0)?);
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user