mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 16:05:01 +02:00
Split the plugin crate (#12563)
# Description This breaks `nu-plugin` up into four crates: - `nu-plugin-protocol`: just the type definitions for the protocol, no I/O. If someone wanted to wire up something more bare metal, maybe for async I/O, they could use this. - `nu-plugin-core`: the shared stuff between engine/plugin. Less stable interface. - `nu-plugin-engine`: everything required for the engine to talk to plugins. Less stable interface. - `nu-plugin`: everything required for the plugin to talk to the engine, what plugin developers use. Should be the most stable interface. No changes are made to the interface exposed by `nu-plugin` - it should all still be there. Re-exports from `nu-plugin-protocol` or `nu-plugin-core` are used as required. Plugins shouldn't ever have to use those crates directly. This should be somewhat faster to compile as `nu-plugin-engine` and `nu-plugin` can compile in parallel, and the engine doesn't need `nu-plugin` and plugins don't need `nu-plugin-engine` (except for test support), so that should reduce what needs to be compiled too. The only significant change here other than splitting stuff up was to break the `source` out of `PluginCustomValue` and create a new `PluginCustomValueWithSource` type that contains that instead. One bonus of that is we get rid of the option and it's now more type-safe, but it also means that the logic for that stuff (actually running the plugin for custom value ops) can live entirely within the `nu-plugin-engine` crate. # User-Facing Changes - New crates. - Added `local-socket` feature for `nu` to try to make it possible to compile without that support if needed. # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib`
This commit is contained in:
412
crates/nu-plugin-protocol/src/evaluated_call.rs
Normal file
412
crates/nu-plugin-protocol/src/evaluated_call.rs
Normal file
@ -0,0 +1,412 @@
|
||||
use nu_protocol::{
|
||||
ast::{Call, Expression},
|
||||
engine::{EngineState, Stack},
|
||||
FromValue, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A representation of the plugin's invocation command including command line args
|
||||
///
|
||||
/// The `EvaluatedCall` contains information about the way a `Plugin` was invoked representing the
|
||||
/// [`Span`] corresponding to the invocation as well as the arguments it was invoked with. It is
|
||||
/// one of the items passed to `PluginCommand::run()`, along with the plugin reference, the engine
|
||||
/// interface, and a [`Value`] that represents the input.
|
||||
///
|
||||
/// The evaluated call is used with the Plugins because the plugin doesn't have
|
||||
/// access to the Stack and the EngineState the way a built in command might. For that
|
||||
/// reason, before encoding the message to the plugin all the arguments to the original
|
||||
/// call (which are expressions) are evaluated and passed to Values
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EvaluatedCall {
|
||||
/// Span of the command invocation
|
||||
pub head: Span,
|
||||
/// Values of positional arguments
|
||||
pub positional: Vec<Value>,
|
||||
/// Names and values of named arguments
|
||||
pub named: Vec<(Spanned<String>, Option<Value>)>,
|
||||
}
|
||||
|
||||
impl EvaluatedCall {
|
||||
/// Try to create an [`EvaluatedCall`] from a command `Call`.
|
||||
pub fn try_from_call(
|
||||
call: &Call,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
|
||||
) -> Result<Self, ShellError> {
|
||||
let positional =
|
||||
call.rest_iter_flattened(0, |expr| eval_expression_fn(engine_state, stack, expr))?;
|
||||
|
||||
let mut named = Vec::with_capacity(call.named_len());
|
||||
for (string, _, expr) in call.named_iter() {
|
||||
let value = match expr {
|
||||
None => None,
|
||||
Some(expr) => Some(eval_expression_fn(engine_state, stack, expr)?),
|
||||
};
|
||||
|
||||
named.push((string.clone(), value))
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
head: call.head,
|
||||
positional,
|
||||
named,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a flag (named parameter that does not take a value) is set
|
||||
/// Returns Ok(true) if flag is set or passed true value
|
||||
/// Returns Ok(false) if flag is not set or passed false value
|
||||
/// Returns Err if passed value is not a boolean
|
||||
///
|
||||
/// # Examples
|
||||
/// Invoked as `my_command --foo`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # None
|
||||
/// # )],
|
||||
/// # };
|
||||
/// assert!(call.has_flag("foo").unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command --bar`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "bar".to_owned(), span: null_span},
|
||||
/// # None
|
||||
/// # )],
|
||||
/// # };
|
||||
/// assert!(!call.has_flag("foo").unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command --foo=true`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::bool(true, Span::unknown()))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// assert!(call.has_flag("foo").unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command --foo=false`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::bool(false, Span::unknown()))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// assert!(!call.has_flag("foo").unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// Invoked with wrong type as `my_command --foo=1`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::int(1, Span::unknown()))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// assert!(call.has_flag("foo").is_err());
|
||||
/// ```
|
||||
pub fn has_flag(&self, flag_name: &str) -> Result<bool, ShellError> {
|
||||
for name in &self.named {
|
||||
if flag_name == name.0.item {
|
||||
return match &name.1 {
|
||||
Some(Value::Bool { val, .. }) => Ok(*val),
|
||||
None => Ok(true),
|
||||
Some(result) => Err(ShellError::CantConvert {
|
||||
to_type: "bool".into(),
|
||||
from_type: result.get_type().to_string(),
|
||||
span: result.span(),
|
||||
help: Some("".into()),
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Returns the [`Value`] of an optional named argument
|
||||
///
|
||||
/// # Examples
|
||||
/// Invoked as `my_command --foo 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::int(123, null_span))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// let opt_foo = match call.get_flag_value("foo") {
|
||||
/// Some(Value::Int { val, .. }) => Some(val),
|
||||
/// None => None,
|
||||
/// _ => panic!(),
|
||||
/// };
|
||||
/// assert_eq!(opt_foo, Some(123));
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![],
|
||||
/// # };
|
||||
/// let opt_foo = match call.get_flag_value("foo") {
|
||||
/// Some(Value::Int { val, .. }) => Some(val),
|
||||
/// None => None,
|
||||
/// _ => panic!(),
|
||||
/// };
|
||||
/// assert_eq!(opt_foo, None);
|
||||
/// ```
|
||||
pub fn get_flag_value(&self, flag_name: &str) -> Option<Value> {
|
||||
for name in &self.named {
|
||||
if flag_name == name.0.item {
|
||||
return name.1.clone();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the [`Value`] of a given (zero indexed) positional argument, if present
|
||||
///
|
||||
/// Examples:
|
||||
/// Invoked as `my_command a b c`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: vec![
|
||||
/// # Value::string("a".to_owned(), null_span),
|
||||
/// # Value::string("b".to_owned(), null_span),
|
||||
/// # Value::string("c".to_owned(), null_span),
|
||||
/// # ],
|
||||
/// # named: vec![],
|
||||
/// # };
|
||||
/// let arg = match call.nth(1) {
|
||||
/// Some(Value::String { val, .. }) => val,
|
||||
/// _ => panic!(),
|
||||
/// };
|
||||
/// assert_eq!(arg, "b".to_owned());
|
||||
///
|
||||
/// let arg = call.nth(7);
|
||||
/// assert!(arg.is_none());
|
||||
/// ```
|
||||
pub fn nth(&self, pos: usize) -> Option<Value> {
|
||||
self.positional.get(pos).cloned()
|
||||
}
|
||||
|
||||
/// Returns the value of a named argument interpreted as type `T`
|
||||
///
|
||||
/// # Examples
|
||||
/// Invoked as `my_command --foo 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::int(123, null_span))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// let foo = call.get_flag::<i64>("foo");
|
||||
/// assert_eq!(foo.unwrap(), Some(123));
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command --bar 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "bar".to_owned(), span: null_span},
|
||||
/// # Some(Value::int(123, null_span))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// let foo = call.get_flag::<i64>("foo");
|
||||
/// assert_eq!(foo.unwrap(), None);
|
||||
/// ```
|
||||
///
|
||||
/// Invoked as `my_command --foo abc`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: Vec::new(),
|
||||
/// # named: vec![(
|
||||
/// # Spanned { item: "foo".to_owned(), span: null_span},
|
||||
/// # Some(Value::string("abc".to_owned(), null_span))
|
||||
/// # )],
|
||||
/// # };
|
||||
/// let foo = call.get_flag::<i64>("foo");
|
||||
/// assert!(foo.is_err());
|
||||
/// ```
|
||||
pub fn get_flag<T: FromValue>(&self, name: &str) -> Result<Option<T>, ShellError> {
|
||||
if let Some(value) = self.get_flag_value(name) {
|
||||
FromValue::from_value(value).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the Nth and all following positional arguments as type `T`
|
||||
///
|
||||
/// # Example
|
||||
/// Invoked as `my_command zero one two three`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
/// # positional: vec![
|
||||
/// # Value::string("zero".to_owned(), null_span),
|
||||
/// # Value::string("one".to_owned(), null_span),
|
||||
/// # Value::string("two".to_owned(), null_span),
|
||||
/// # Value::string("three".to_owned(), null_span),
|
||||
/// # ],
|
||||
/// # named: Vec::new(),
|
||||
/// # };
|
||||
/// let args = call.rest::<String>(0);
|
||||
/// assert_eq!(args.unwrap(), vec!["zero", "one", "two", "three"]);
|
||||
///
|
||||
/// let args = call.rest::<String>(2);
|
||||
/// assert_eq!(args.unwrap(), vec!["two", "three"]);
|
||||
/// ```
|
||||
pub fn rest<T: FromValue>(&self, starting_pos: usize) -> Result<Vec<T>, ShellError> {
|
||||
self.positional
|
||||
.iter()
|
||||
.skip(starting_pos)
|
||||
.map(|value| FromValue::from_value(value.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Retrieve the value of an optional positional argument interpreted as type `T`
|
||||
///
|
||||
/// Returns the value of a (zero indexed) positional argument of type `T`.
|
||||
/// Alternatively returns [`None`] if the positional argument does not exist
|
||||
/// or an error that can be passed back to the shell on error.
|
||||
pub fn opt<T: FromValue>(&self, pos: usize) -> Result<Option<T>, ShellError> {
|
||||
if let Some(value) = self.nth(pos) {
|
||||
FromValue::from_value(value).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve the value of a mandatory positional argument as type `T`
|
||||
///
|
||||
/// Expect a positional argument of type `T` and return its value or, if the
|
||||
/// argument does not exist or is of the wrong type, return an error that can
|
||||
/// be passed back to the shell.
|
||||
pub fn req<T: FromValue>(&self, pos: usize) -> Result<T, ShellError> {
|
||||
if let Some(value) = self.nth(pos) {
|
||||
FromValue::from_value(value)
|
||||
} else if self.positional.is_empty() {
|
||||
Err(ShellError::AccessEmptyContent { span: self.head })
|
||||
} else {
|
||||
Err(ShellError::AccessBeyondEnd {
|
||||
max_idx: self.positional.len() - 1,
|
||||
span: self.head,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use nu_protocol::{Span, Spanned, Value};
|
||||
|
||||
#[test]
|
||||
fn call_to_value() {
|
||||
let call = EvaluatedCall {
|
||||
head: Span::new(0, 10),
|
||||
positional: vec![
|
||||
Value::float(1.0, Span::new(0, 10)),
|
||||
Value::string("something", Span::new(0, 10)),
|
||||
],
|
||||
named: vec![
|
||||
(
|
||||
Spanned {
|
||||
item: "name".to_string(),
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
Some(Value::float(1.0, Span::new(0, 10))),
|
||||
),
|
||||
(
|
||||
Spanned {
|
||||
item: "flag".to_string(),
|
||||
span: Span::new(0, 10),
|
||||
},
|
||||
None,
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
let name: Option<f64> = call.get_flag("name").unwrap();
|
||||
assert_eq!(name, Some(1.0));
|
||||
|
||||
assert!(call.has_flag("flag").unwrap());
|
||||
|
||||
let required: f64 = call.req(0).unwrap();
|
||||
assert!((required - 1.0).abs() < f64::EPSILON);
|
||||
|
||||
let optional: Option<String> = call.opt(1).unwrap();
|
||||
assert_eq!(optional, Some("something".to_string()));
|
||||
|
||||
let rest: Vec<String> = call.rest(1).unwrap();
|
||||
assert_eq!(rest, vec!["something".to_string()]);
|
||||
}
|
||||
}
|
611
crates/nu-plugin-protocol/src/lib.rs
Normal file
611
crates/nu-plugin-protocol/src/lib.rs
Normal file
@ -0,0 +1,611 @@
|
||||
//! Type definitions, including full `Serialize` and `Deserialize` implementations, for the protocol
|
||||
//! used for communication between the engine and a plugin.
|
||||
//!
|
||||
//! See the [plugin protocol reference](https://www.nushell.sh/contributor-book/plugin_protocol_reference.html)
|
||||
//! for more details on what exactly is being specified here.
|
||||
//!
|
||||
//! Plugins accept messages of [`PluginInput`] and send messages back of [`PluginOutput`]. This
|
||||
//! crate explicitly avoids implementing any functionality that depends on I/O, so the exact
|
||||
//! byte-level encoding scheme is not implemented here. See the protocol ref or `nu_plugin_core` for
|
||||
//! more details on how that works.
|
||||
|
||||
mod evaluated_call;
|
||||
mod plugin_custom_value;
|
||||
mod protocol_info;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Things that can help with protocol-related tests. Not part of the public API, just used by other
|
||||
/// nushell crates.
|
||||
#[doc(hidden)]
|
||||
pub mod test_util;
|
||||
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::Closure, Config, LabeledError, PipelineData, PluginSignature, RawStream,
|
||||
ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use evaluated_call::EvaluatedCall;
|
||||
pub use plugin_custom_value::PluginCustomValue;
|
||||
#[allow(unused_imports)] // may be unused by compile flags
|
||||
pub use protocol_info::{Feature, Protocol, ProtocolInfo};
|
||||
|
||||
/// A sequential identifier for a stream
|
||||
pub type StreamId = usize;
|
||||
|
||||
/// A sequential identifier for a [`PluginCall`]
|
||||
pub type PluginCallId = usize;
|
||||
|
||||
/// A sequential identifier for an [`EngineCall`]
|
||||
pub type EngineCallId = 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,
|
||||
/// Pipeline input. This is usually [`nu_protocol::PipelineData`] or [`PipelineDataHeader`]
|
||||
pub input: D,
|
||||
}
|
||||
|
||||
impl<D> CallInfo<D> {
|
||||
/// Convert the type of `input` from `D` to `T`.
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<CallInfo<T>, ShellError> {
|
||||
Ok(CallInfo {
|
||||
name: self.name,
|
||||
call: self.call,
|
||||
input: f(self.input)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
/// Initiate [`nu_protocol::PipelineData::ListStream`].
|
||||
///
|
||||
/// Items are sent via [`StreamData`]
|
||||
ListStream(ListStreamInfo),
|
||||
/// Initiate [`nu_protocol::PipelineData::ExternalStream`].
|
||||
///
|
||||
/// Items are sent via [`StreamData`]
|
||||
ExternalStream(ExternalStreamInfo),
|
||||
}
|
||||
|
||||
impl PipelineDataHeader {
|
||||
/// Return a list of stream IDs embedded in the header
|
||||
pub fn stream_ids(&self) -> Vec<StreamId> {
|
||||
match self {
|
||||
PipelineDataHeader::Empty => vec![],
|
||||
PipelineDataHeader::Value(_) => vec![],
|
||||
PipelineDataHeader::ListStream(info) => vec![info.id],
|
||||
PipelineDataHeader::ExternalStream(info) => {
|
||||
let mut out = vec![];
|
||||
if let Some(stdout) = &info.stdout {
|
||||
out.push(stdout.id);
|
||||
}
|
||||
if let Some(stderr) = &info.stderr {
|
||||
out.push(stderr.id);
|
||||
}
|
||||
if let Some(exit_code) = &info.exit_code {
|
||||
out.push(exit_code.id);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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,
|
||||
Run(CallInfo<D>),
|
||||
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
|
||||
}
|
||||
|
||||
impl<D> PluginCall<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<PluginCall<T>, ShellError> {
|
||||
Ok(match self {
|
||||
PluginCall::Signature => PluginCall::Signature,
|
||||
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
|
||||
PluginCall::CustomValueOp(custom_value, op) => {
|
||||
PluginCall::CustomValueOp(custom_value, op)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The span associated with the call.
|
||||
pub fn span(&self) -> Option<Span> {
|
||||
match self {
|
||||
PluginCall::Signature => None,
|
||||
PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
|
||||
PluginCall::CustomValueOp(val, _) => Some(val.span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations supported for custom values.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum CustomValueOp {
|
||||
/// [`to_base_value()`](nu_protocol::CustomValue::to_base_value)
|
||||
ToBaseValue,
|
||||
/// [`follow_path_int()`](nu_protocol::CustomValue::follow_path_int)
|
||||
FollowPathInt(Spanned<usize>),
|
||||
/// [`follow_path_string()`](nu_protocol::CustomValue::follow_path_string)
|
||||
FollowPathString(Spanned<String>),
|
||||
/// [`partial_cmp()`](nu_protocol::CustomValue::partial_cmp)
|
||||
PartialCmp(Value),
|
||||
/// [`operation()`](nu_protocol::CustomValue::operation)
|
||||
Operation(Spanned<Operator>, Value),
|
||||
/// Notify that the custom value has been dropped, if
|
||||
/// [`notify_plugin_on_drop()`](nu_protocol::CustomValue::notify_plugin_on_drop) is true
|
||||
Dropped,
|
||||
}
|
||||
|
||||
impl CustomValueOp {
|
||||
/// Get the name of the op, for error messages.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
CustomValueOp::ToBaseValue => "to_base_value",
|
||||
CustomValueOp::FollowPathInt(_) => "follow_path_int",
|
||||
CustomValueOp::FollowPathString(_) => "follow_path_string",
|
||||
CustomValueOp::PartialCmp(_) => "partial_cmp",
|
||||
CustomValueOp::Operation(_, _) => "operation",
|
||||
CustomValueOp::Dropped => "dropped",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>),
|
||||
/// Don't expect any more plugin calls. Exit after all currently executing plugin calls are
|
||||
/// finished.
|
||||
Goodbye,
|
||||
/// Response to an [`EngineCall`]. The ID should be the same one sent with the engine call this
|
||||
/// is responding to
|
||||
EngineCallResponse(EngineCallId, EngineCallResponse<PipelineDataHeader>),
|
||||
/// See [`StreamMessage::Data`].
|
||||
Data(StreamId, StreamData),
|
||||
/// See [`StreamMessage::End`].
|
||||
End(StreamId),
|
||||
/// See [`StreamMessage::Drop`].
|
||||
Drop(StreamId),
|
||||
/// See [`StreamMessage::Ack`].
|
||||
Ack(StreamId),
|
||||
}
|
||||
|
||||
impl TryFrom<PluginInput> for StreamMessage {
|
||||
type Error = PluginInput;
|
||||
|
||||
fn try_from(msg: PluginInput) -> Result<StreamMessage, PluginInput> {
|
||||
match msg {
|
||||
PluginInput::Data(id, data) => Ok(StreamMessage::Data(id, data)),
|
||||
PluginInput::End(id) => Ok(StreamMessage::End(id)),
|
||||
PluginInput::Drop(id) => Ok(StreamMessage::Drop(id)),
|
||||
PluginInput::Ack(id) => Ok(StreamMessage::Ack(id)),
|
||||
_ => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for PluginInput {
|
||||
fn from(stream_msg: StreamMessage) -> PluginInput {
|
||||
match stream_msg {
|
||||
StreamMessage::Data(id, data) => PluginInput::Data(id, data),
|
||||
StreamMessage::End(id) => PluginInput::End(id),
|
||||
StreamMessage::Drop(id) => PluginInput::Drop(id),
|
||||
StreamMessage::Ack(id) => PluginInput::Ack(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single item of stream data for a stream.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum StreamData {
|
||||
List(Value),
|
||||
Raw(Result<Vec<u8>, LabeledError>),
|
||||
}
|
||||
|
||||
impl From<Value> for StreamData {
|
||||
fn from(value: Value) -> Self {
|
||||
StreamData::List(value)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fn from(value: Result<Vec<u8>, ShellError>) -> Self {
|
||||
value.map_err(LabeledError::from).into()
|
||||
}
|
||||
}
|
||||
|
||||
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>, LabeledError> {
|
||||
type Error = ShellError;
|
||||
|
||||
fn try_from(data: StreamData) -> Result<Result<Vec<u8>, LabeledError>, ShellError> {
|
||||
match data {
|
||||
StreamData::Raw(value) => Ok(value),
|
||||
StreamData::List(_) => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "expected raw stream data, found list data".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[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),
|
||||
}
|
||||
|
||||
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginCallResponse<D> {
|
||||
Error(LabeledError),
|
||||
Signature(Vec<PluginSignature>),
|
||||
Ordering(Option<Ordering>),
|
||||
PipelineData(D),
|
||||
}
|
||||
|
||||
impl<D> PluginCallResponse<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<PluginCallResponse<T>, ShellError> {
|
||||
Ok(match self {
|
||||
PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
|
||||
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
|
||||
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
|
||||
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginCallResponse<PipelineData> {
|
||||
/// Does this response have a stream?
|
||||
pub fn has_stream(&self) -> bool {
|
||||
match self {
|
||||
PluginCallResponse::PipelineData(data) => match data {
|
||||
PipelineData::Empty => false,
|
||||
PipelineData::Value(..) => false,
|
||||
PipelineData::ListStream(..) => true,
|
||||
PipelineData::ExternalStream { .. } => true,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options that can be changed to affect how the engine treats the plugin
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginOption {
|
||||
/// Send `GcDisabled(true)` to stop the plugin from being automatically garbage collected, or
|
||||
/// `GcDisabled(false)` to enable it again.
|
||||
///
|
||||
/// See `EngineInterface::set_gc_disabled()` in `nu-plugin` for more information.
|
||||
GcDisabled(bool),
|
||||
}
|
||||
|
||||
/// This is just a serializable version of [`std::cmp::Ordering`], and can be converted 1:1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum Ordering {
|
||||
Less,
|
||||
Equal,
|
||||
Greater,
|
||||
}
|
||||
|
||||
impl From<std::cmp::Ordering> for Ordering {
|
||||
fn from(value: std::cmp::Ordering) -> Self {
|
||||
match value {
|
||||
std::cmp::Ordering::Less => Ordering::Less,
|
||||
std::cmp::Ordering::Equal => Ordering::Equal,
|
||||
std::cmp::Ordering::Greater => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ordering> for std::cmp::Ordering {
|
||||
fn from(value: Ordering) -> Self {
|
||||
match value {
|
||||
Ordering::Less => std::cmp::Ordering::Less,
|
||||
Ordering::Equal => std::cmp::Ordering::Equal,
|
||||
Ordering::Greater => std::cmp::Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information received from the plugin
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginOutput {
|
||||
/// This must be the first message. Indicates supported protocol
|
||||
Hello(ProtocolInfo),
|
||||
/// Set option. No response expected
|
||||
Option(PluginOption),
|
||||
/// 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>),
|
||||
/// Execute an [`EngineCall`]. Engine calls must be executed within the `context` of a plugin
|
||||
/// call, and the `id` should not have been used before
|
||||
EngineCall {
|
||||
/// The plugin call (by ID) to execute in the context of
|
||||
context: PluginCallId,
|
||||
/// A new identifier for this engine call. The response will reference this ID
|
||||
id: EngineCallId,
|
||||
call: EngineCall<PipelineDataHeader>,
|
||||
},
|
||||
/// See [`StreamMessage::Data`].
|
||||
Data(StreamId, StreamData),
|
||||
/// See [`StreamMessage::End`].
|
||||
End(StreamId),
|
||||
/// See [`StreamMessage::Drop`].
|
||||
Drop(StreamId),
|
||||
/// See [`StreamMessage::Ack`].
|
||||
Ack(StreamId),
|
||||
}
|
||||
|
||||
impl TryFrom<PluginOutput> for StreamMessage {
|
||||
type Error = PluginOutput;
|
||||
|
||||
fn try_from(msg: PluginOutput) -> Result<StreamMessage, PluginOutput> {
|
||||
match msg {
|
||||
PluginOutput::Data(id, data) => Ok(StreamMessage::Data(id, data)),
|
||||
PluginOutput::End(id) => Ok(StreamMessage::End(id)),
|
||||
PluginOutput::Drop(id) => Ok(StreamMessage::Drop(id)),
|
||||
PluginOutput::Ack(id) => Ok(StreamMessage::Ack(id)),
|
||||
_ => Err(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for PluginOutput {
|
||||
fn from(stream_msg: StreamMessage) -> PluginOutput {
|
||||
match stream_msg {
|
||||
StreamMessage::Data(id, data) => PluginOutput::Data(id, data),
|
||||
StreamMessage::End(id) => PluginOutput::End(id),
|
||||
StreamMessage::Drop(id) => PluginOutput::Drop(id),
|
||||
StreamMessage::Ack(id) => PluginOutput::Ack(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A remote call back to the engine during the plugin's execution.
|
||||
///
|
||||
/// The type parameter determines the input type, for calls that take pipeline data.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum EngineCall<D> {
|
||||
/// Get the full engine configuration
|
||||
GetConfig,
|
||||
/// Get the plugin-specific configuration (`$env.config.plugins.NAME`)
|
||||
GetPluginConfig,
|
||||
/// Get an environment variable
|
||||
GetEnvVar(String),
|
||||
/// Get all environment variables
|
||||
GetEnvVars,
|
||||
/// Get current working directory
|
||||
GetCurrentDir,
|
||||
/// Set an environment variable in the caller's scope
|
||||
AddEnvVar(String, Value),
|
||||
/// Get help for the current command
|
||||
GetHelp,
|
||||
/// Move the plugin into the foreground for terminal interaction
|
||||
EnterForeground,
|
||||
/// Move the plugin out of the foreground once terminal interaction has finished
|
||||
LeaveForeground,
|
||||
/// Get the contents of a span. Response is a binary which may not parse to UTF-8
|
||||
GetSpanContents(Span),
|
||||
/// Evaluate a closure with stream input/output
|
||||
EvalClosure {
|
||||
/// The closure to call.
|
||||
///
|
||||
/// This may come from a [`Value::Closure`] passed in as an argument to the plugin.
|
||||
closure: Spanned<Closure>,
|
||||
/// Positional arguments to add to the closure call
|
||||
positional: Vec<Value>,
|
||||
/// Input to the closure
|
||||
input: D,
|
||||
/// Whether to redirect stdout from external commands
|
||||
redirect_stdout: bool,
|
||||
/// Whether to redirect stderr from external commands
|
||||
redirect_stderr: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<D> EngineCall<D> {
|
||||
/// Get the name of the engine call so it can be embedded in things like error messages
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
EngineCall::GetConfig => "GetConfig",
|
||||
EngineCall::GetPluginConfig => "GetPluginConfig",
|
||||
EngineCall::GetEnvVar(_) => "GetEnv",
|
||||
EngineCall::GetEnvVars => "GetEnvs",
|
||||
EngineCall::GetCurrentDir => "GetCurrentDir",
|
||||
EngineCall::AddEnvVar(..) => "AddEnvVar",
|
||||
EngineCall::GetHelp => "GetHelp",
|
||||
EngineCall::EnterForeground => "EnterForeground",
|
||||
EngineCall::LeaveForeground => "LeaveForeground",
|
||||
EngineCall::GetSpanContents(_) => "GetSpanContents",
|
||||
EngineCall::EvalClosure { .. } => "EvalClosure",
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<EngineCall<T>, ShellError> {
|
||||
Ok(match self {
|
||||
EngineCall::GetConfig => EngineCall::GetConfig,
|
||||
EngineCall::GetPluginConfig => EngineCall::GetPluginConfig,
|
||||
EngineCall::GetEnvVar(name) => EngineCall::GetEnvVar(name),
|
||||
EngineCall::GetEnvVars => EngineCall::GetEnvVars,
|
||||
EngineCall::GetCurrentDir => EngineCall::GetCurrentDir,
|
||||
EngineCall::AddEnvVar(name, value) => EngineCall::AddEnvVar(name, value),
|
||||
EngineCall::GetHelp => EngineCall::GetHelp,
|
||||
EngineCall::EnterForeground => EngineCall::EnterForeground,
|
||||
EngineCall::LeaveForeground => EngineCall::LeaveForeground,
|
||||
EngineCall::GetSpanContents(span) => EngineCall::GetSpanContents(span),
|
||||
EngineCall::EvalClosure {
|
||||
closure,
|
||||
positional,
|
||||
input,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
} => EngineCall::EvalClosure {
|
||||
closure,
|
||||
positional,
|
||||
input: f(input)?,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The response to an [`EngineCall`]. The type parameter determines the output type for pipeline
|
||||
/// data.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum EngineCallResponse<D> {
|
||||
Error(ShellError),
|
||||
PipelineData(D),
|
||||
Config(Box<Config>),
|
||||
ValueMap(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
impl<D> EngineCallResponse<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<EngineCallResponse<T>, ShellError> {
|
||||
Ok(match self {
|
||||
EngineCallResponse::Error(err) => EngineCallResponse::Error(err),
|
||||
EngineCallResponse::PipelineData(data) => EngineCallResponse::PipelineData(f(data)?),
|
||||
EngineCallResponse::Config(config) => EngineCallResponse::Config(config),
|
||||
EngineCallResponse::ValueMap(map) => EngineCallResponse::ValueMap(map),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EngineCallResponse<PipelineData> {
|
||||
/// Build an [`EngineCallResponse::PipelineData`] from a [`Value`]
|
||||
pub fn value(value: Value) -> EngineCallResponse<PipelineData> {
|
||||
EngineCallResponse::PipelineData(PipelineData::Value(value, None))
|
||||
}
|
||||
|
||||
/// An [`EngineCallResponse::PipelineData`] with [`PipelineData::Empty`]
|
||||
pub const fn empty() -> EngineCallResponse<PipelineData> {
|
||||
EngineCallResponse::PipelineData(PipelineData::Empty)
|
||||
}
|
||||
}
|
236
crates/nu-plugin-protocol/src/plugin_custom_value/mod.rs
Normal file
236
crates/nu-plugin-protocol/src/plugin_custom_value/mod.rs
Normal file
@ -0,0 +1,236 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use nu_protocol::{ast::Operator, CustomValue, ShellError, Span, Value};
|
||||
use nu_utils::SharedCow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// An opaque container for a custom value that is handled fully by a plugin.
|
||||
///
|
||||
/// This is the only type of custom value that is allowed to cross the plugin serialization
|
||||
/// boundary.
|
||||
///
|
||||
/// The plugin is responsible for ensuring that local plugin custom values are converted to and from
|
||||
/// [`PluginCustomValue`] on the boundary.
|
||||
///
|
||||
/// The engine is responsible for adding tracking the source of the custom value, ensuring that only
|
||||
/// [`PluginCustomValue`] is contained within any values sent, and that the source of any values
|
||||
/// sent matches the plugin it is being sent to.
|
||||
///
|
||||
/// Most of the [`CustomValue`] methods on this type will result in a panic. The source must be
|
||||
/// added (see `nu_plugin_engine::PluginCustomValueWithSource`) in order to implement the
|
||||
/// functionality via plugin calls.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluginCustomValue(SharedCow<SharedContent>);
|
||||
|
||||
/// Content shared across copies of a plugin custom value.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct SharedContent {
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
name: String,
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
data: Vec<u8>,
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
///
|
||||
/// This is not serialized if `false`, since most custom values don't need it.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
notify_on_drop: bool,
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for PluginCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
self.clone().into_value(span)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
self.name().to_owned()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, _span: Span) -> Result<Value, ShellError> {
|
||||
panic!("to_base_value() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn follow_path_int(
|
||||
&self,
|
||||
_self_span: Span,
|
||||
_index: usize,
|
||||
_path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("follow_path_int() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn follow_path_string(
|
||||
&self,
|
||||
_self_span: Span,
|
||||
_column_name: String,
|
||||
_path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("follow_path_string() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn partial_cmp(&self, _other: &Value) -> Option<Ordering> {
|
||||
panic!("partial_cmp() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn operation(
|
||||
&self,
|
||||
_lhs_span: Span,
|
||||
_operator: Operator,
|
||||
_op_span: Span,
|
||||
_right: &Value,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("operation() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginCustomValue {
|
||||
/// Create a new [`PluginCustomValue`].
|
||||
pub fn new(name: String, data: Vec<u8>, notify_on_drop: bool) -> PluginCustomValue {
|
||||
PluginCustomValue(SharedCow::new(SharedContent {
|
||||
name,
|
||||
data,
|
||||
notify_on_drop,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a [`Value`] containing this custom value.
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self), span)
|
||||
}
|
||||
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0.name
|
||||
}
|
||||
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.0.data
|
||||
}
|
||||
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
pub fn notify_on_drop(&self) -> bool {
|
||||
self.0.notify_on_drop
|
||||
}
|
||||
|
||||
/// Count the number of shared copies of this [`PluginCustomValue`].
|
||||
pub fn ref_count(&self) -> usize {
|
||||
SharedCow::ref_count(&self.0)
|
||||
}
|
||||
|
||||
/// Serialize a custom value into a [`PluginCustomValue`]. This should only be done on the
|
||||
/// plugin side.
|
||||
pub fn serialize_from_custom_value(
|
||||
custom_value: &dyn CustomValue,
|
||||
span: Span,
|
||||
) -> Result<PluginCustomValue, ShellError> {
|
||||
let name = custom_value.type_name();
|
||||
let notify_on_drop = custom_value.notify_plugin_on_drop();
|
||||
bincode::serialize(custom_value)
|
||||
.map(|data| PluginCustomValue::new(name, data, notify_on_drop))
|
||||
.map_err(|err| ShellError::CustomValueFailedToEncode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a [`PluginCustomValue`] into a `Box<dyn CustomValue>`. This should only be done
|
||||
/// on the plugin side.
|
||||
pub 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,
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { 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(Box::new(serialized), span);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
if let Some(val) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
let deserialized = val.deserialize_to_custom_value(span)?;
|
||||
*value = Value::custom(deserialized, span);
|
||||
Ok(())
|
||||
} else {
|
||||
// Already not a PluginCustomValue
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Render any custom values in the `Value` using `to_base_value()`
|
||||
pub fn render_to_base_value_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
*value = val.to_base_value(span)?;
|
||||
Ok(())
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
259
crates/nu-plugin-protocol/src/plugin_custom_value/tests.rs
Normal file
259
crates/nu-plugin-protocol/src/plugin_custom_value/tests.rs
Normal file
@ -0,0 +1,259 @@
|
||||
use crate::test_util::{expected_test_custom_value, test_plugin_custom_value, TestCustomValue};
|
||||
|
||||
use super::PluginCustomValue;
|
||||
use nu_protocol::{engine::Closure, record, CustomValue, ShellError, Span, Value};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn check_closure_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let closure = val.as_closure()?;
|
||||
for index in indices {
|
||||
let val = closure
|
||||
.captures
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in closure"));
|
||||
let custom_value = val
|
||||
.1
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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.type_name(), serialized.name());
|
||||
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 serialize_in_root() -> Result<(), ShellError> {
|
||||
let span = Span::new(4, 10);
|
||||
let mut val = Value::custom(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()
|
||||
);
|
||||
} else {
|
||||
panic!("Failed to downcast to PluginCustomValue");
|
||||
}
|
||||
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 serialize_in_closure() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(24)));
|
||||
let mut val = Value::test_closure(Closure {
|
||||
block_id: 0,
|
||||
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
|
||||
});
|
||||
PluginCustomValue::serialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_closure_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(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_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(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_in_closure() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_closure(Closure {
|
||||
block_id: 0,
|
||||
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
|
||||
});
|
||||
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
|
||||
|
||||
check_closure_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(())
|
||||
})
|
||||
}
|
106
crates/nu-plugin-protocol/src/protocol_info.rs
Normal file
106
crates/nu-plugin-protocol/src/protocol_info.rs
Normal file
@ -0,0 +1,106 @@
|
||||
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-protocol`
|
||||
/// 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: default_features(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolInfo {
|
||||
/// True if the version specified in `self` is compatible with the version specified in `other`.
|
||||
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]))
|
||||
}
|
||||
|
||||
/// True if the protocol info contains a feature compatible with the given feature.
|
||||
pub fn supports_feature(&self, feature: &Feature) -> bool {
|
||||
self.features.iter().any(|f| feature.is_compatible_with(f))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// The plugin supports running with a local socket passed via `--local-socket` instead of
|
||||
/// stdio.
|
||||
LocalSocket,
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Feature {
|
||||
/// True if the feature is considered to be compatible with another feature.
|
||||
pub fn is_compatible_with(&self, other: &Feature) -> bool {
|
||||
matches!((self, other), (Feature::LocalSocket, Feature::LocalSocket))
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol features compiled into this version of `nu-plugin`.
|
||||
pub fn default_features() -> Vec<Feature> {
|
||||
vec![
|
||||
// Only available if compiled with the `local-socket` feature flag (enabled by default).
|
||||
#[cfg(feature = "local-socket")]
|
||||
Feature::LocalSocket,
|
||||
]
|
||||
}
|
43
crates/nu-plugin-protocol/src/test_util.rs
Normal file
43
crates/nu-plugin-protocol/src/test_util.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use crate::PluginCustomValue;
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A custom value that can be used for testing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TestCustomValue(pub i32);
|
||||
|
||||
#[typetag::serde(name = "nu_plugin_protocol::test_util::TestCustomValue")]
|
||||
impl CustomValue for TestCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self.clone()), span)
|
||||
}
|
||||
|
||||
fn type_name(&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
|
||||
}
|
||||
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`TestCustomValue`] serialized as a [`PluginCustomValue`].
|
||||
pub 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::new("TestCustomValue".into(), data, false)
|
||||
}
|
||||
|
||||
/// The expected [`TestCustomValue`] that [`test_plugin_custom_value()`] should deserialize into.
|
||||
pub fn expected_test_custom_value() -> TestCustomValue {
|
||||
TestCustomValue(-1)
|
||||
}
|
35
crates/nu-plugin-protocol/src/tests.rs
Normal file
35
crates/nu-plugin-protocol/src/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