diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index bec0fb536..186652d76 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -1,7 +1,52 @@ +#![allow(clippy::needless_doctest_main)] +//! # Nu Plugin: Plugin library for Nushell +//! +//! This crate contains the interface necessary to build Nushell plugins in Rust. +//! Additionally, it contains public, but undocumented, items used by Nushell itself +//! to interface with Nushell plugins. This documentation focuses on the interface +//! needed to write an independent plugin. +//! +//! Nushell plugins are stand-alone applications that communicate with Nushell +//! over stdin and stdout using a standardizes serialization framework to exchange +//! the typed data that Nushell commands utilize natively. +//! +//! A typical plugin application will define a struct that implements the [Plugin] +//! trait and then, in it's main method, pass that [Plugin] to the [serve_plugin] +//! function, which will handle all of the input and output serialization when +//! invoked by Nushell. +//! +//! ``` +//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, Plugin, serve_plugin}; +//! use nu_protocol::{PluginSignature, Value}; +//! +//! struct MyPlugin; +//! +//! impl Plugin for MyPlugin { +//! fn signature(&self) -> Vec { +//! todo!(); +//! } +//! fn run( +//! &mut self, +//! name: &str, +//! call: &EvaluatedCall, +//! input: &Value +//! ) -> Result { +//! todo!(); +//! } +//! } +//! +//! fn main() { +//! serve_plugin(&mut MyPlugin{}, MsgPackSerializer) +//! } +//! ``` +//! +//! Nushell's source tree contains a +//! [Plugin Example](https://github.com/nushell/nushell/tree/main/crates/nu_plugin_example) +//! that demonstrates the full range of plugin capabilities. mod plugin; mod protocol; mod serializers; pub use plugin::{get_signature, serve_plugin, Plugin, PluginDeclaration}; -pub use protocol::{EvaluatedCall, LabeledError, PluginData, PluginResponse}; +pub use protocol::{EvaluatedCall, LabeledError, PluginResponse}; pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer, EncodingType}; diff --git a/crates/nu-plugin/src/plugin/declaration.rs b/crates/nu-plugin/src/plugin/declaration.rs index ab17c4bd7..1059284a3 100644 --- a/crates/nu-plugin/src/plugin/declaration.rs +++ b/crates/nu-plugin/src/plugin/declaration.rs @@ -10,6 +10,7 @@ use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ast::Call, PluginSignature, Signature}; use nu_protocol::{Example, PipelineData, ShellError, Value}; +#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser #[derive(Clone)] pub struct PluginDeclaration { name: String, diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index c341f2b94..c6cdf40a1 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -17,23 +17,31 @@ use super::EvaluatedCall; pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192; +/// Encoding scheme that defines a plugin's communication protocol with Nu pub trait PluginEncoder: Clone { + /// The name of the encoder (e.g., `json`) fn name(&self) -> &str; + /// Serialize a `PluginCall` in the `PluginEncoder`s format fn encode_call( &self, plugin_call: &PluginCall, writer: &mut impl std::io::Write, ) -> Result<(), ShellError>; + /// Deserialize a `PluginCall` from the `PluginEncoder`s format fn decode_call(&self, reader: &mut impl std::io::BufRead) -> Result; + /// Serialize a `PluginResponse` from the plugin in this `PluginEncoder`'s preferred + /// format fn encode_response( &self, plugin_response: &PluginResponse, writer: &mut impl std::io::Write, ) -> Result<(), ShellError>; + /// Deserialize a `PluginResponse` from the plugin from this `PluginEncoder`'s + /// preferred format fn decode_response( &self, reader: &mut impl std::io::BufRead, @@ -113,6 +121,7 @@ pub(crate) fn call_plugin( } } +#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser pub fn get_signature( path: &Path, shell: &Option, @@ -179,10 +188,58 @@ pub fn get_signature( } } -// The next trait and functions are part of the plugin that is being created -// The `Plugin` trait defines the API which plugins use to "hook" into nushell. +/// The basic API for a Nushell plugin +/// +/// This is the trait that Nushell plugins must implement. The methods defined on +/// `Plugin` are invoked by [serve_plugin] during plugin registration and execution. +/// +/// # Examples +/// Basic usage: +/// ``` +/// # use nu_plugin::*; +/// # use nu_protocol::{PluginSignature, Type, Value}; +/// struct HelloPlugin; +/// +/// impl Plugin for HelloPlugin { +/// fn signature(&self) -> Vec { +/// let sig = PluginSignature::build("hello") +/// .output_type(Type::String); +/// +/// vec![sig] +/// } +/// +/// fn run( +/// &mut self, +/// name: &str, +/// call: &EvaluatedCall, +/// input: &Value, +/// ) -> Result { +/// Ok(Value::String { +/// val: "Hello, World!".to_owned(), +/// span: call.head, +/// }) +/// } +/// } +/// ``` pub trait Plugin { + /// The signature of the plugin + /// + /// This method returns the [PluginSignature]s that describe the capabilities + /// of this plugin. Since a single plugin executable can support multiple invocation + /// patterns we return a `Vec` of signatures. fn signature(&self) -> Vec; + + /// Perform the actual behavior of the plugin + /// + /// The behavior of the plugin is defined by the implementation of this method. + /// When Nushell invoked the plugin [serve_plugin] will call this method and + /// print the serialized returned value or error to stdout, which Nushell will + /// interpret. + /// + /// The `name` is only relevant for plugins that implement multiple commands as the + /// invoked command will be passed in via this argument. The `call` contains + /// metadata describing how the plugin was invoked and `input` contains the structured + /// data passed to the command implemented by this [Plugin]. fn run( &mut self, name: &str, @@ -191,23 +248,30 @@ pub trait Plugin { ) -> Result; } -// Function used in the plugin definition for the communication protocol between -// nushell and the external plugin. -// When creating a new plugin you have to use this function as the main -// entry point for the plugin, e.g. -// -// fn main() { -// serve_plugin(plugin) -// } -// -// where plugin is your struct that implements the Plugin trait -// -// Note. When defining a plugin in other language but Rust, you will have to compile -// the plugin.capnp schema to create the object definitions that will be returned from -// the plugin. -// The object that is expected to be received by nushell is the PluginResponse struct. -// That should be encoded correctly and sent to StdOut for nushell to decode and -// and present its result +/// Function used to implement the communication protocol between +/// nushell and an external plugin. +/// +/// When creating a new plugin this function is typically used as the main entry +/// point for the plugin, e.g. +/// +/// ``` +/// # use nu_plugin::*; +/// # use nu_protocol::{PluginSignature, Value}; +/// # struct MyPlugin; +/// # impl MyPlugin { fn new() -> Self { Self }} +/// # impl Plugin for MyPlugin { +/// # fn signature(&self) -> Vec {todo!();} +/// # fn run(&mut self, name: &str, call: &EvaluatedCall, input: &Value) +/// # -> Result {todo!();} +/// # } +/// fn main() { +/// serve_plugin(&mut MyPlugin::new(), MsgPackSerializer) +/// } +/// ``` +/// +/// The object that is expected to be received by nushell is the `PluginResponse` struct. +/// The `serve_plugin` function should ensure that it is encoded correctly and sent +/// to StdOut for nushell to decode and and present its result. pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) { if env::args().any(|arg| (arg == "-h") || (arg == "--help")) { print_help(plugin, encoder); diff --git a/crates/nu-plugin/src/protocol/evaluated_call.rs b/crates/nu-plugin/src/protocol/evaluated_call.rs index b7ea8456c..b60252c03 100644 --- a/crates/nu-plugin/src/protocol/evaluated_call.rs +++ b/crates/nu-plugin/src/protocol/evaluated_call.rs @@ -6,19 +6,29 @@ use nu_protocol::{ }; use serde::{Deserialize, Serialize}; -// The evaluated call is used with the Plugins because the plugin doesn't have -// access to the Stack and the EngineState. 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 +/// A representation of the plugin's invocation command including command line args +/// +/// The `EvaluatedCall` contains information about the way a [Plugin](crate::Plugin) was invoked +/// representing the [`Span`] corresponding to the invocation as well as the arguments +/// it was invoked with. It is one of three items passed to [`run`](crate::Plugin::run()) along with +/// `name` which command that was invoked 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, + /// Names and values of named arguments pub named: Vec<(Spanned, Option)>, } impl EvaluatedCall { - pub fn try_from_call( + pub(crate) fn try_from_call( call: &Call, engine_state: &EngineState, stack: &mut Stack, @@ -45,6 +55,43 @@ impl EvaluatedCall { }) } + /// Indicates whether named parameter is present in the arguments + /// + /// Typically this method would be used on a flag parameter, a named parameter + /// that does not take a value. + /// + /// # Examples + /// Invoked as `my_command --foo`: + /// ``` + /// # use nu_protocol::{Spanned, Span, Value}; + /// # use nu_plugin::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")); + /// ``` + /// + /// Invoked as `my_command --bar`: + /// ``` + /// # use nu_protocol::{Spanned, Span, Value}; + /// # use nu_plugin::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")); + /// ``` pub fn has_flag(&self, flag_name: &str) -> bool { for name in &self.named { if flag_name == name.0.item { @@ -55,6 +102,47 @@ impl EvaluatedCall { 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::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 { val: 123, span: 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::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 { for name in &self.named { if flag_name == name.0.item { @@ -65,10 +153,89 @@ impl EvaluatedCall { 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::EvaluatedCall; + /// # let null_span = Span::new(0, 0); + /// # let call = EvaluatedCall { + /// # head: null_span, + /// # positional: vec![ + /// # Value::String { val: "a".to_owned(), span: null_span }, + /// # Value::String { val: "b".to_owned(), span: null_span }, + /// # Value::String { val: "c".to_owned(), span: 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 { 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::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 { val: 123, span: null_span }) + /// # )], + /// # }; + /// let foo = call.get_flag::("foo"); + /// assert_eq!(foo.unwrap(), Some(123)); + /// ``` + /// + /// Invoked as `my_command --bar 123`: + /// ``` + /// # use nu_protocol::{Spanned, Span, Value}; + /// # use nu_plugin::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 { val: 123, span: null_span }) + /// # )], + /// # }; + /// let foo = call.get_flag::("foo"); + /// assert_eq!(foo.unwrap(), None); + /// ``` + /// + /// Invoked as `my_command --foo abc`: + /// ``` + /// # use nu_protocol::{Spanned, Span, Value}; + /// # use nu_plugin::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 { val: "abc".to_owned(), span: null_span }) + /// # )], + /// # }; + /// let foo = call.get_flag::("foo"); + /// assert!(foo.is_err()); + /// ``` pub fn get_flag(&self, name: &str) -> Result, ShellError> { if let Some(value) = self.get_flag_value(name) { FromValue::from_value(&value).map(Some) @@ -77,6 +244,30 @@ impl EvaluatedCall { } } + /// 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::EvaluatedCall; + /// # let null_span = Span::new(0, 0); + /// # let call = EvaluatedCall { + /// # head: null_span, + /// # positional: vec![ + /// # Value::String { val: "zero".to_owned(), span: null_span }, + /// # Value::String { val: "one".to_owned(), span: null_span }, + /// # Value::String { val: "two".to_owned(), span: null_span }, + /// # Value::String { val: "three".to_owned(), span: null_span }, + /// # ], + /// # named: Vec::new(), + /// # }; + /// let args = call.rest::(0); + /// assert_eq!(args.unwrap(), vec!["zero", "one", "two", "three"]); + /// + /// let args = call.rest::(2); + /// assert_eq!(args.unwrap(), vec!["two", "three"]); + /// ``` pub fn rest(&self, starting_pos: usize) -> Result, ShellError> { self.positional .iter() @@ -85,6 +276,11 @@ impl EvaluatedCall { .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(&self, pos: usize) -> Result, ShellError> { if let Some(value) = self.nth(pos) { FromValue::from_value(&value).map(Some) @@ -93,6 +289,11 @@ impl EvaluatedCall { } } + /// 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(&self, pos: usize) -> Result { if let Some(value) = self.nth(pos) { FromValue::from_value(&value) diff --git a/crates/nu-plugin/src/protocol/mod.rs b/crates/nu-plugin/src/protocol/mod.rs index dcae6411b..faab118b2 100644 --- a/crates/nu-plugin/src/protocol/mod.rs +++ b/crates/nu-plugin/src/protocol/mod.rs @@ -29,10 +29,19 @@ pub enum PluginCall { CollapseCustomValue(PluginData), } +/// An error message with debugging information that can be passed to Nushell from the plugin +/// +/// The `LabeledError` struct is a structured error message that can be returned from +/// a [Plugin](crate::Plugin)'s [`run`](crate::Plugin::run()) method. It contains +/// the error message along with optional [Span] data to support highlighting in the +/// shell. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] pub struct LabeledError { + /// The name of the error pub label: String, + /// A detailed error description pub msg: String, + /// The [Span] in which the error occurred pub span: Option, } @@ -99,6 +108,9 @@ impl From for LabeledError { } // Information received from the plugin +// Needs to be public to communicate with nu-parser but not typically +// used by Plugin authors +#[doc(hidden)] #[derive(Serialize, Deserialize)] pub enum PluginResponse { Error(LabeledError), diff --git a/crates/nu-plugin/src/serializers/json.rs b/crates/nu-plugin/src/serializers/json.rs index 3a9cd1ac3..7793f03ab 100644 --- a/crates/nu-plugin/src/serializers/json.rs +++ b/crates/nu-plugin/src/serializers/json.rs @@ -2,6 +2,8 @@ use nu_protocol::ShellError; use crate::{plugin::PluginEncoder, protocol::PluginResponse}; +/// A `PluginEncoder` that enables the plugin to communicate with Nushel with JSON +/// serialized data. #[derive(Clone, Debug)] pub struct JsonSerializer; diff --git a/crates/nu-plugin/src/serializers/mod.rs b/crates/nu-plugin/src/serializers/mod.rs index 5df43ab1d..ef0c39c63 100644 --- a/crates/nu-plugin/src/serializers/mod.rs +++ b/crates/nu-plugin/src/serializers/mod.rs @@ -7,6 +7,7 @@ use nu_protocol::ShellError; pub mod json; pub mod msgpack; +#[doc(hidden)] #[derive(Clone, Debug)] pub enum EncodingType { Json(json::JsonSerializer), diff --git a/crates/nu-plugin/src/serializers/msgpack.rs b/crates/nu-plugin/src/serializers/msgpack.rs index ff4aa08e0..e881ce944 100644 --- a/crates/nu-plugin/src/serializers/msgpack.rs +++ b/crates/nu-plugin/src/serializers/msgpack.rs @@ -1,6 +1,8 @@ use crate::{plugin::PluginEncoder, protocol::PluginResponse}; use nu_protocol::ShellError; +/// A `PluginEncoder` that enables the plugin to communicate with Nushel with MsgPack +/// serialized data. #[derive(Clone, Debug)] pub struct MsgPackSerializer; diff --git a/crates/nu-protocol/src/plugin_signature.rs b/crates/nu-protocol/src/plugin_signature.rs index 96e2d2e1d..0185adefe 100644 --- a/crates/nu-protocol/src/plugin_signature.rs +++ b/crates/nu-protocol/src/plugin_signature.rs @@ -5,7 +5,7 @@ use serde::Serialize; use crate::engine::Command; use crate::{BlockId, Category, Flag, PositionalArg, SyntaxShape, Type}; -/// A simple wrapper for Signature, includes examples. +/// A simple wrapper for Signature that includes examples. #[derive(Clone, Serialize, Deserialize)] pub struct PluginSignature { pub sig: Signature, @@ -23,13 +23,13 @@ impl PluginSignature { Self { sig, examples } } - // Add a default help option to a signature + /// Add a default help option to a signature pub fn add_help(mut self) -> PluginSignature { self.sig = self.sig.add_help(); self } - // Build an internal signature with default help option + /// Build an internal signature with default help option pub fn build(name: impl Into) -> PluginSignature { let sig = Signature::new(name.into()).add_help(); Self::new(sig, vec![]) @@ -94,7 +94,6 @@ impl PluginSignature { desc: impl Into, ) -> PluginSignature { self.sig = self.sig.rest(name, shape, desc); - self } @@ -112,7 +111,6 @@ impl PluginSignature { short: Option, ) -> PluginSignature { self.sig = self.sig.named(name, shape, desc, short); - self } @@ -165,7 +163,6 @@ impl PluginSignature { /// Changes the signature category pub fn category(mut self, category: Category) -> PluginSignature { self.sig = self.sig.category(category); - self }