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:
Devyn Cairns
2024-04-27 10:08:12 -07:00
committed by GitHub
parent 884d5312bb
commit 0c4d5330ee
74 changed files with 3514 additions and 3110 deletions

View 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()]);
}
}

View 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)
}
}

View 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(()),
}
})
}
}

View 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(())
})
}

View 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,
]
}

View 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)
}

View 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(())
}