diff --git a/Cargo.lock b/Cargo.lock index 856f4ddcc2..9704b52301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,15 +393,6 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae9b8a7119420b5279ddc2b4ee553ee15bcf4605df6135a26f03ffe153bee97c" -[[package]] -name = "capnpc" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47bce811162518b5c38f746ed584bd2922ae7bb560ef64f230d2e4ee0d111fe" -dependencies = [ - "capnp", -] - [[package]] name = "cc" version = "1.0.72" @@ -1752,9 +1743,10 @@ name = "nu-plugin" version = "0.1.0" dependencies = [ "capnp", - "capnpc", "nu-engine", "nu-protocol", + "serde", + "serde_json", ] [[package]] diff --git a/crates/nu-command/src/core_commands/register.rs b/crates/nu-command/src/core_commands/register.rs index 57ed9605a8..85d843e848 100644 --- a/crates/nu-command/src/core_commands/register.rs +++ b/crates/nu-command/src/core_commands/register.rs @@ -19,9 +19,19 @@ impl Command for Register { .required( "plugin", SyntaxShape::Filepath, - "location of bin for plugin", + "path of executable for plugin", + ) + .required_named( + "encoding", + SyntaxShape::String, + "Encoding used to communicate with plugin. Options: [capnp, json]", + Some('e'), + ) + .optional( + "signature", + SyntaxShape::Any, + "Block with signature description as json object", ) - .optional("signature", SyntaxShape::Any, "plugin signature") .category(Category::Core) } diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index 24d9cd7371..187f1d4c26 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -65,6 +65,10 @@ pub enum ParseError { )] UnexpectedKeyword(String, #[label("unexpected {0}")] Span), + #[error("Incorrect value")] + #[diagnostic(code(nu::parser::incorrect_value), url(docsrs), help("{2}"))] + IncorrectValue(String, #[label("unexpected {0}")] Span, String), + #[error("Multiple rest params.")] #[diagnostic(code(nu::parser::multiple_rest_params), url(docsrs))] MultipleRestParams(#[label = "multiple rest params"] Span), @@ -187,9 +191,9 @@ pub enum ParseError { #[diagnostic(code(nu::parser::export_not_found), url(docsrs))] ExportNotFound(#[label = "could not find imports"] Span), - #[error("File not found: {0}")] + #[error("File not found")] #[diagnostic(code(nu::parser::file_not_found), url(docsrs))] - FileNotFound(String), + FileNotFound(String, #[label("File not found: {0}")] Span), #[error("{0}")] #[diagnostic()] diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 0986195d36..34a7eb032d 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -12,7 +12,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ lex, lite_parse, parser::{ - check_name, garbage, garbage_statement, parse, parse_block_expression, + check_call, check_name, garbage, garbage_statement, parse, parse_block_expression, parse_import_pattern, parse_internal_call, parse_signature, parse_string, trim_quotes, }, ParseError, @@ -719,7 +719,10 @@ pub fn parse_use( ); } } else { - error = error.or(Some(ParseError::FileNotFound(module_filename))); + error = error.or(Some(ParseError::FileNotFound( + module_filename, + import_pattern.head.span, + ))); (ImportPattern::new(), Overlay::new()) } } else { @@ -1059,7 +1062,7 @@ pub fn parse_source( } } } else { - error = error.or(Some(ParseError::FileNotFound(filename))); + error = error.or(Some(ParseError::FileNotFound(filename, spans[1]))); } } else { return ( @@ -1093,14 +1096,12 @@ pub fn parse_register( working_set: &mut StateWorkingSet, spans: &[Span], ) -> (Statement, Option) { - use std::{path::PathBuf, str::FromStr}; - - use nu_plugin::plugin::{get_signature, PluginDeclaration}; + use nu_plugin::{get_signature, EncodingType, PluginDeclaration}; use nu_protocol::Signature; - let name = working_set.get_span_contents(spans[0]); - - if name != b"register" { + // Checking that the function is used with the correct name + // Maybe this is not necessary but it is a sanity check + if working_set.get_span_contents(spans[0]) != b"register" { return ( garbage_statement(spans), Some(ParseError::UnknownState( @@ -1110,119 +1111,132 @@ pub fn parse_register( ); } - if let Some(decl_id) = working_set.find_decl(b"register") { - let (call, call_span, mut err) = - parse_internal_call(working_set, spans[0], &spans[1..], decl_id); - - let error = { - match spans.len() { - 1 => Some(ParseError::MissingPositional( - "plugin location".into(), - spans[0], + // Parsing the spans and checking that they match the register signature + // Using a parsed call makes more sense than checking for how many spans are in the call + // Also, by creating a call, it can be checked if it matches the declaration signature + let (call, call_span) = match working_set.find_decl(b"register") { + None => { + return ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: Register declaration not found".into(), + span(spans), )), - 2 => { - let name_expr = working_set.get_span_contents(spans[1]); - String::from_utf8(name_expr.to_vec()) - .map_err(|_| ParseError::NonUtf8(spans[1])) - .and_then(|name| { - canonicalize(&name).map_err(|e| ParseError::FileNotFound(e.to_string())) - }) - .and_then(|path| { - if path.exists() & path.is_file() { - get_signature(path.as_path()) - .map_err(|err| { - ParseError::LabeledError( - "Error getting signatures".into(), - err.to_string(), - spans[0], - ) - }) - .map(|signatures| (path, signatures)) - } else { - Err(ParseError::FileNotFound(format!("{:?}", path))) - } - }) - .map(|(path, signatures)| { - for signature in signatures { - // create plugin command declaration (need struct impl Command) - // store declaration in working set - let plugin_decl = PluginDeclaration::new(path.clone(), signature); + ) + } + Some(decl_id) => { + let (call, call_span, mut err) = + parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + let decl = working_set.get_decl(decl_id); - working_set.add_decl(Box::new(plugin_decl)); - } - - working_set.mark_plugins_file_dirty(); - }) - .err() - } - 3 => { - let filename_slice = working_set.get_span_contents(spans[1]); - let signature = working_set.get_span_contents(spans[2]); - - String::from_utf8(filename_slice.to_vec()) - .map_err(|_| ParseError::NonUtf8(spans[1])) - .and_then(|name| { - PathBuf::from_str(name.as_str()).map_err(|_| { - ParseError::InternalError( - format!("Unable to create path from string {}", name), - spans[0], - ) - }) - }) - .and_then(|path_inner| { - serde_json::from_slice::(signature) - .map_err(|_| { - ParseError::LabeledError( - "Signature deserialization error".into(), - "unable to deserialize signature".into(), - spans[0], - ) - }) - .map(|signature| (path_inner, signature)) - }) - .and_then(|(path, signature)| { - if path.exists() & path.is_file() { - let plugin_decl = PluginDeclaration::new(path, signature); - - working_set.add_decl(Box::new(plugin_decl)); - - working_set.mark_plugins_file_dirty(); - Ok(()) - } else { - Err(ParseError::FileNotFound(format!("{:?}", path))) - } - }) - .err() - } - _ => { - let span = spans[3..].iter().fold(spans[3], |acc, next| Span { - start: acc.start, - end: next.end, - }); - - Some(ParseError::ExtraPositional(span)) - } + err = check_call(call_span, &decl.signature(), &call).or(err); + if err.is_some() { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); } - }; - err = error.or(err); + (call, call_span) + } + }; - ( - Statement::Pipeline(Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Unknown, - custom_completion: None, - }])), - err, - ) - } else { - ( - garbage_statement(spans), - Some(ParseError::UnknownState( - "internal error: Register declaration not found".into(), - span(spans), - )), - ) + // Extracting the required arguments from the call and keeping them together in a tuple + // The ? operator is not used because the error has to be kept to be printed in the shell + // For that reason the values are kept in a result that will be passed at the end of this call + let arguments = call + .positional + .get(0) + .map(|expr| { + let name_expr = working_set.get_span_contents(expr.span); + String::from_utf8(name_expr.to_vec()) + .map_err(|_| ParseError::NonUtf8(spans[1])) + .and_then(|name| { + canonicalize(&name).map_err(|_| ParseError::FileNotFound(name, expr.span)) + }) + .and_then(|path| { + if path.exists() & path.is_file() { + Ok(path) + } else { + Err(ParseError::FileNotFound(format!("{:?}", path), expr.span)) + } + }) + }) + .expect("required positional has being checked") + .and_then(|path| { + call.get_flag_expr("encoding") + .map(|expr| { + EncodingType::try_from_bytes(working_set.get_span_contents(expr.span)) + .ok_or_else(|| { + ParseError::IncorrectValue( + "wrong encoding".into(), + expr.span, + "Encodings available: capnp and json".into(), + ) + }) + }) + .expect("required named has being checked") + .map(|encoding| (path, encoding)) + }); + + // Signature is the only optional value from the call and will be used to decide if + // the plugin is called to get the signatures or to use the given signature + let signature = call.positional.get(1).map(|expr| { + let signature = working_set.get_span_contents(expr.span); + serde_json::from_slice::(signature).map_err(|_| { + ParseError::LabeledError( + "Signature deserialization error".into(), + "unable to deserialize signature".into(), + spans[0], + ) + }) + }); + + let error = match signature { + Some(signature) => arguments.and_then(|(path, encoding)| { + signature.map(|signature| { + let plugin_decl = PluginDeclaration::new(path, signature, encoding); + working_set.add_decl(Box::new(plugin_decl)); + working_set.mark_plugins_file_dirty(); + }) + }), + None => arguments.and_then(|(path, encoding)| { + get_signature(path.as_path(), &encoding) + .map_err(|err| { + ParseError::LabeledError( + "Error getting signatures".into(), + err.to_string(), + spans[0], + ) + }) + .map(|signatures| { + for signature in signatures { + // create plugin command declaration (need struct impl Command) + // store declaration in working set + let plugin_decl = + PluginDeclaration::new(path.clone(), signature, encoding.clone()); + + working_set.add_decl(Box::new(plugin_decl)); + } + + working_set.mark_plugins_file_dirty(); + }) + }), } + .err(); + + ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Nothing, + custom_completion: None, + }])), + error, + ) } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 0e30e6187c..a82d9d621d 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -82,7 +82,7 @@ pub fn trim_quotes(bytes: &[u8]) -> &[u8] { } } -fn check_call(command: Span, sig: &Signature, call: &Call) -> Option { +pub fn check_call(command: Span, sig: &Signature, call: &Call) -> Option { // Allow the call to pass if they pass in the help flag if call.named.iter().any(|(n, _)| n.item == "help") { return None; diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index df6e6236e3..fc5dcc627a 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -7,8 +7,8 @@ edition = "2018" capnp = "0.14.3" nu-protocol = { path = "../nu-protocol" } nu-engine = { path = "../nu-engine" } - -[build-dependencies] -capnpc = "0.14.3" +serde = {version = "1.0.130", features = ["derive"]} +serde_json = { version = "1.0"} + diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index 04db8b604d..de50aa1ff2 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -1,7 +1,10 @@ -pub mod evaluated_call; -pub mod plugin; -pub mod plugin_capnp; -pub mod serializers; +mod plugin; +mod protocol; +mod serializers; -pub use evaluated_call::EvaluatedCall; -pub use plugin::{serve_plugin, LabeledError, Plugin}; +#[allow(dead_code)] +mod plugin_capnp; + +pub use plugin::{get_signature, serve_plugin, Plugin, PluginDeclaration}; +pub use protocol::{EvaluatedCall, LabeledError}; +pub use serializers::{capnp::CapnpSerializer, json::JsonSerializer, EncodingType}; diff --git a/crates/nu-plugin/src/plugin.rs b/crates/nu-plugin/src/plugin.rs deleted file mode 100644 index df816bee0e..0000000000 --- a/crates/nu-plugin/src/plugin.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::serializers::{decode_call, decode_response, encode_call, encode_response}; -use std::io::BufReader; -use std::path::{Path, PathBuf}; -use std::process::{Command as CommandSys, Stdio}; - -use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{ast::Call, Signature, Value}; -use nu_protocol::{PipelineData, ShellError, Span}; - -use super::evaluated_call::EvaluatedCall; - -const OUTPUT_BUFFER_SIZE: usize = 8192; - -#[derive(Debug)] -pub struct CallInfo { - pub name: String, - pub call: EvaluatedCall, - pub input: Value, -} - -// Information sent to the plugin -#[derive(Debug)] -pub enum PluginCall { - Signature, - CallInfo(Box), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct LabeledError { - pub label: String, - pub msg: String, - pub span: Option, -} - -impl From for ShellError { - fn from(error: LabeledError) -> Self { - match error.span { - Some(span) => ShellError::SpannedLabeledError(error.label, error.msg, span), - None => ShellError::LabeledError(error.label, error.msg), - } - } -} - -impl From for LabeledError { - fn from(error: ShellError) -> Self { - match error { - ShellError::SpannedLabeledError(label, msg, span) => LabeledError { - label, - msg, - span: Some(span), - }, - ShellError::LabeledError(label, msg) => LabeledError { - label, - msg, - span: None, - }, - ShellError::CantConvert(expected, input, span) => LabeledError { - label: format!("Can't convert to {}", expected), - msg: format!("can't convert {} to {}", expected, input), - span: Some(span), - }, - ShellError::DidYouMean(suggestion, span) => LabeledError { - label: "Name not found".into(), - msg: format!("did you mean '{}'", suggestion), - span: Some(span), - }, - ShellError::PluginFailedToLoad(msg) => LabeledError { - label: "Plugin failed to load".into(), - msg, - span: None, - }, - ShellError::PluginFailedToEncode(msg) => LabeledError { - label: "Plugin failed to encode".into(), - msg, - span: None, - }, - ShellError::PluginFailedToDecode(msg) => LabeledError { - label: "Plugin failed to decode".into(), - msg, - span: None, - }, - err => LabeledError { - label: "Error - Add to LabeledError From".into(), - msg: err.to_string(), - span: None, - }, - } - } -} - -// Information received from the plugin -#[derive(Debug)] -pub enum PluginResponse { - Error(LabeledError), - Signature(Vec), - Value(Box), -} - -pub fn get_signature(path: &Path) -> Result, ShellError> { - let mut plugin_cmd = create_command(path); - - let mut child = plugin_cmd.spawn().map_err(|err| { - ShellError::PluginFailedToLoad(format!("Error spawning child process: {}", err)) - })?; - - // Create message to plugin to indicate that signature is required and - // send call to plugin asking for signature - if let Some(stdin_writer) = &mut child.stdin { - let mut writer = stdin_writer; - encode_call(&PluginCall::Signature, &mut writer)? - } - - // deserialize response from plugin to extract the signature - let signature = if let Some(stdout_reader) = &mut child.stdout { - let reader = stdout_reader; - let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); - let response = decode_response(&mut buf_read)?; - - match response { - PluginResponse::Signature(sign) => Ok(sign), - PluginResponse::Error(err) => Err(err.into()), - _ => Err(ShellError::PluginFailedToLoad( - "Plugin missing signature".into(), - )), - } - } else { - Err(ShellError::PluginFailedToLoad( - "Plugin missing stdout reader".into(), - )) - }?; - - // There is no need to wait for the child process to finish since the - // signature has being collected - Ok(signature) -} - -fn create_command(path: &Path) -> CommandSys { - //TODO. The selection of shell could be modifiable from the config file. - let mut process = if cfg!(windows) { - let mut process = CommandSys::new("cmd"); - process.arg("/c").arg(path); - - process - } else { - let mut process = CommandSys::new("sh"); - process.arg("-c").arg(path); - - process - }; - - // Both stdout and stdin are piped so we can receive information from the plugin - process.stdout(Stdio::piped()).stdin(Stdio::piped()); - - process -} - -#[derive(Debug, Clone)] -pub struct PluginDeclaration { - name: String, - signature: Signature, - filename: PathBuf, -} - -impl PluginDeclaration { - pub fn new(filename: PathBuf, signature: Signature) -> Self { - Self { - name: signature.name.clone(), - signature, - filename, - } - } -} - -impl Command for PluginDeclaration { - fn name(&self) -> &str { - &self.name - } - - fn signature(&self) -> Signature { - self.signature.clone() - } - - fn usage(&self) -> &str { - self.signature.usage.as_str() - } - - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, - ) -> Result { - // Call the command with self path - // Decode information from plugin - // Create PipelineData - let source_file = Path::new(&self.filename); - let mut plugin_cmd = create_command(source_file); - - let mut child = plugin_cmd.spawn().map_err(|err| { - let decl = engine_state.get_decl(call.decl_id); - ShellError::SpannedLabeledError( - format!("Unable to spawn plugin for {}", decl.name()), - format!("{}", err), - call.head, - ) - })?; - - let input = match input { - PipelineData::Value(value, ..) => value, - PipelineData::Stream(stream, ..) => { - let values = stream.collect::>(); - - Value::List { - vals: values, - span: call.head, - } - } - }; - - // Create message to plugin to indicate that signature is required and - // send call to plugin asking for signature - if let Some(stdin_writer) = &mut child.stdin { - // PluginCall information - let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { - name: self.name.clone(), - call: EvaluatedCall::try_from_call(call, engine_state, stack)?, - input, - })); - - let mut writer = stdin_writer; - - encode_call(&plugin_call, &mut writer).map_err(|err| { - let decl = engine_state.get_decl(call.decl_id); - ShellError::SpannedLabeledError( - format!("Unable to encode call for {}", decl.name()), - err.to_string(), - call.head, - ) - })?; - } - - // Deserialize response from plugin to extract the resulting value - let pipeline_data = if let Some(stdout_reader) = &mut child.stdout { - let reader = stdout_reader; - let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); - let response = decode_response(&mut buf_read).map_err(|err| { - let decl = engine_state.get_decl(call.decl_id); - ShellError::SpannedLabeledError( - format!("Unable to decode call for {}", decl.name()), - err.to_string(), - call.head, - ) - })?; - - match response { - PluginResponse::Value(value) => { - Ok(PipelineData::Value(value.as_ref().clone(), None)) - } - PluginResponse::Error(err) => Err(err.into()), - PluginResponse::Signature(..) => Err(ShellError::SpannedLabeledError( - "Plugin missing value".into(), - "Received a signature from plugin instead of value".into(), - call.head, - )), - } - } else { - Err(ShellError::SpannedLabeledError( - "Error with stdout reader".into(), - "no stdout reader".into(), - call.head, - )) - }?; - - // There is no need to wait for the child process to finish - // The response has been collected from the plugin call - Ok(pipeline_data) - } - - fn is_plugin(&self) -> Option<&PathBuf> { - Some(&self.filename) - } -} - -// 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. -pub trait Plugin { - fn signature(&self) -> Vec; - fn run( - &mut self, - name: &str, - call: &EvaluatedCall, - input: &Value, - ) -> 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 -// -pub fn serve_plugin(plugin: &mut impl Plugin) { - let mut stdin_buf = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, std::io::stdin()); - let plugin_call = decode_call(&mut stdin_buf); - - match plugin_call { - Err(err) => { - let response = PluginResponse::Error(err.into()); - encode_response(&response, &mut std::io::stdout()).expect("Error encoding response"); - } - Ok(plugin_call) => { - match plugin_call { - // Sending the signature back to nushell to create the declaration definition - PluginCall::Signature => { - let response = PluginResponse::Signature(plugin.signature()); - encode_response(&response, &mut std::io::stdout()) - .expect("Error encoding response"); - } - PluginCall::CallInfo(call_info) => { - let value = plugin.run(&call_info.name, &call_info.call, &call_info.input); - - let response = match value { - Ok(value) => PluginResponse::Value(Box::new(value)), - Err(err) => PluginResponse::Error(err), - }; - encode_response(&response, &mut std::io::stdout()) - .expect("Error encoding response"); - } - } - } - } -} diff --git a/crates/nu-plugin/src/plugin/declaration.rs b/crates/nu-plugin/src/plugin/declaration.rs new file mode 100644 index 0000000000..67ca2be1a6 --- /dev/null +++ b/crates/nu-plugin/src/plugin/declaration.rs @@ -0,0 +1,137 @@ +use crate::{EncodingType, EvaluatedCall}; + +use super::{create_command, OUTPUT_BUFFER_SIZE}; +use crate::protocol::{CallInfo, PluginCall, PluginResponse}; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ast::Call, Signature, Value}; +use nu_protocol::{PipelineData, ShellError}; + +#[derive(Clone)] +pub struct PluginDeclaration { + name: String, + signature: Signature, + filename: PathBuf, + encoding: EncodingType, +} + +impl PluginDeclaration { + pub fn new(filename: PathBuf, signature: Signature, encoding: EncodingType) -> Self { + Self { + name: signature.name.clone(), + signature, + filename, + encoding, + } + } +} + +impl Command for PluginDeclaration { + fn name(&self) -> &str { + &self.name + } + + fn signature(&self) -> Signature { + self.signature.clone() + } + + fn usage(&self) -> &str { + self.signature.usage.as_str() + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // Call the command with self path + // Decode information from plugin + // Create PipelineData + let source_file = Path::new(&self.filename); + let mut plugin_cmd = create_command(source_file); + + let mut child = plugin_cmd.spawn().map_err(|err| { + let decl = engine_state.get_decl(call.decl_id); + ShellError::SpannedLabeledError( + format!("Unable to spawn plugin for {}", decl.name()), + format!("{}", err), + call.head, + ) + })?; + + let input = match input { + PipelineData::Value(value, ..) => value, + PipelineData::Stream(stream, ..) => { + let values = stream.collect::>(); + + Value::List { + vals: values, + span: call.head, + } + } + }; + + // Create message to plugin to indicate that signature is required and + // send call to plugin asking for signature + if let Some(mut stdin_writer) = child.stdin.take() { + let encoding_clone = self.encoding.clone(); + let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { + name: self.name.clone(), + call: EvaluatedCall::try_from_call(call, engine_state, stack)?, + input, + })); + std::thread::spawn(move || { + // PluginCall information + encoding_clone.encode_call(&plugin_call, &mut stdin_writer) + }); + } + + // Deserialize response from plugin to extract the resulting value + let pipeline_data = if let Some(stdout_reader) = &mut child.stdout { + let reader = stdout_reader; + let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); + + let response = self + .encoding + .decode_response(&mut buf_read) + .map_err(|err| { + let decl = engine_state.get_decl(call.decl_id); + ShellError::SpannedLabeledError( + format!("Unable to decode call for {}", decl.name()), + err.to_string(), + call.head, + ) + })?; + + match response { + PluginResponse::Value(value) => { + Ok(PipelineData::Value(value.as_ref().clone(), None)) + } + PluginResponse::Error(err) => Err(err.into()), + PluginResponse::Signature(..) => Err(ShellError::SpannedLabeledError( + "Plugin missing value".into(), + "Received a signature from plugin instead of value".into(), + call.head, + )), + } + } else { + Err(ShellError::SpannedLabeledError( + "Error with stdout reader".into(), + "no stdout reader".into(), + call.head, + )) + }?; + + // There is no need to wait for the child process to finish + // The response has been collected from the plugin call + Ok(pipeline_data) + } + + fn is_plugin(&self) -> Option<(&PathBuf, &str)> { + Some((&self.filename, self.encoding.to_str())) + } +} diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs new file mode 100644 index 0000000000..de08133816 --- /dev/null +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -0,0 +1,161 @@ +mod declaration; +pub use declaration::PluginDeclaration; + +use crate::protocol::{LabeledError, PluginCall, PluginResponse}; +use crate::EncodingType; +use std::io::BufReader; +use std::path::Path; +use std::process::{Command as CommandSys, Stdio}; + +use nu_protocol::ShellError; +use nu_protocol::{Signature, Value}; + +use super::EvaluatedCall; + +const OUTPUT_BUFFER_SIZE: usize = 8192; + +pub trait PluginEncoder: Clone { + fn encode_call( + &self, + plugin_call: &PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError>; + + fn decode_call(&self, reader: &mut impl std::io::BufRead) -> Result; + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError>; + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result; +} + +fn create_command(path: &Path) -> CommandSys { + //TODO. The selection of shell could be modifiable from the config file. + let mut process = if cfg!(windows) { + let mut process = CommandSys::new("cmd"); + process.arg("/c").arg(path); + + process + } else { + let mut process = CommandSys::new("sh"); + process.arg("-c").arg(path); + + process + }; + + // Both stdout and stdin are piped so we can receive information from the plugin + process.stdout(Stdio::piped()).stdin(Stdio::piped()); + + process +} + +pub fn get_signature(path: &Path, encoding: &EncodingType) -> Result, ShellError> { + let mut plugin_cmd = create_command(path); + + let mut child = plugin_cmd.spawn().map_err(|err| { + ShellError::PluginFailedToLoad(format!("Error spawning child process: {}", err)) + })?; + + // Create message to plugin to indicate that signature is required and + // send call to plugin asking for signature + if let Some(mut stdin_writer) = child.stdin.take() { + let encoding_clone = encoding.clone(); + std::thread::spawn(move || { + encoding_clone.encode_call(&PluginCall::Signature, &mut stdin_writer) + }); + } + + // deserialize response from plugin to extract the signature + let signatures = if let Some(stdout_reader) = &mut child.stdout { + let reader = stdout_reader; + let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader); + let response = encoding.decode_response(&mut buf_read)?; + + match response { + PluginResponse::Signature(sign) => Ok(sign), + PluginResponse::Error(err) => Err(err.into()), + _ => Err(ShellError::PluginFailedToLoad( + "Plugin missing signature".into(), + )), + } + } else { + Err(ShellError::PluginFailedToLoad( + "Plugin missing stdout reader".into(), + )) + }?; + + // There is no need to wait for the child process to finish since the + // signature has being collected + Ok(signatures) +} + +// 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. +pub trait Plugin { + fn signature(&self) -> Vec; + fn run( + &mut self, + name: &str, + call: &EvaluatedCall, + input: &Value, + ) -> 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 +pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) { + let mut stdin_buf = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, std::io::stdin()); + let plugin_call = encoder.decode_call(&mut stdin_buf); + + match plugin_call { + Err(err) => { + let response = PluginResponse::Error(err.into()); + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + Ok(plugin_call) => { + match plugin_call { + // Sending the signature back to nushell to create the declaration definition + PluginCall::Signature => { + let response = PluginResponse::Signature(plugin.signature()); + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + PluginCall::CallInfo(call_info) => { + let value = plugin.run(&call_info.name, &call_info.call, &call_info.input); + + let response = match value { + Ok(value) => PluginResponse::Value(Box::new(value)), + Err(err) => PluginResponse::Error(err), + }; + encoder + .encode_response(&response, &mut std::io::stdout()) + .expect("Error encoding response"); + } + } + } + } +} diff --git a/crates/nu-plugin/src/evaluated_call.rs b/crates/nu-plugin/src/protocol/evaluated_call.rs similarity index 98% rename from crates/nu-plugin/src/evaluated_call.rs rename to crates/nu-plugin/src/protocol/evaluated_call.rs index cfc08274d9..6f70cef641 100644 --- a/crates/nu-plugin/src/evaluated_call.rs +++ b/crates/nu-plugin/src/protocol/evaluated_call.rs @@ -4,12 +4,13 @@ use nu_protocol::{ engine::{EngineState, Stack}, FromValue, ShellError, Span, Spanned, Value, }; +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 -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct EvaluatedCall { pub head: Span, pub positional: Vec, diff --git a/crates/nu-plugin/src/protocol/mod.rs b/crates/nu-plugin/src/protocol/mod.rs new file mode 100644 index 0000000000..ace953a99b --- /dev/null +++ b/crates/nu-plugin/src/protocol/mod.rs @@ -0,0 +1,90 @@ +mod evaluated_call; + +pub use evaluated_call::EvaluatedCall; +use nu_protocol::{ShellError, Signature, Span, Value}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct CallInfo { + pub name: String, + pub call: EvaluatedCall, + pub input: Value, +} + +// Information sent to the plugin +#[derive(Serialize, Deserialize, Debug)] +pub enum PluginCall { + Signature, + CallInfo(Box), +} + +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +pub struct LabeledError { + pub label: String, + pub msg: String, + pub span: Option, +} + +impl From for ShellError { + fn from(error: LabeledError) -> Self { + match error.span { + Some(span) => ShellError::SpannedLabeledError(error.label, error.msg, span), + None => ShellError::LabeledError(error.label, error.msg), + } + } +} + +impl From for LabeledError { + fn from(error: ShellError) -> Self { + match error { + ShellError::SpannedLabeledError(label, msg, span) => LabeledError { + label, + msg, + span: Some(span), + }, + ShellError::LabeledError(label, msg) => LabeledError { + label, + msg, + span: None, + }, + ShellError::CantConvert(expected, input, span) => LabeledError { + label: format!("Can't convert to {}", expected), + msg: format!("can't convert {} to {}", expected, input), + span: Some(span), + }, + ShellError::DidYouMean(suggestion, span) => LabeledError { + label: "Name not found".into(), + msg: format!("did you mean '{}'", suggestion), + span: Some(span), + }, + ShellError::PluginFailedToLoad(msg) => LabeledError { + label: "Plugin failed to load".into(), + msg, + span: None, + }, + ShellError::PluginFailedToEncode(msg) => LabeledError { + label: "Plugin failed to encode".into(), + msg, + span: None, + }, + ShellError::PluginFailedToDecode(msg) => LabeledError { + label: "Plugin failed to decode".into(), + msg, + span: None, + }, + err => LabeledError { + label: "Error - Add to LabeledError From".into(), + msg: err.to_string(), + span: None, + }, + } + } +} + +// Information received from the plugin +#[derive(Serialize, Deserialize)] +pub enum PluginResponse { + Error(LabeledError), + Signature(Vec), + Value(Box), +} diff --git a/crates/nu-plugin/src/serializers/call.rs b/crates/nu-plugin/src/serializers/capnp/call.rs similarity index 98% rename from crates/nu-plugin/src/serializers/call.rs rename to crates/nu-plugin/src/serializers/capnp/call.rs index b898c3fff5..abe3c5fa57 100644 --- a/crates/nu-plugin/src/serializers/call.rs +++ b/crates/nu-plugin/src/serializers/capnp/call.rs @@ -1,5 +1,5 @@ use super::value; -use crate::{evaluated_call::EvaluatedCall, plugin_capnp::evaluated_call}; +use crate::{plugin_capnp::evaluated_call, EvaluatedCall}; use nu_protocol::{ShellError, Span, Spanned, Value}; pub(crate) fn serialize_call( diff --git a/crates/nu-plugin/src/serializers/capnp/mod.rs b/crates/nu-plugin/src/serializers/capnp/mod.rs new file mode 100644 index 0000000000..74707cd327 --- /dev/null +++ b/crates/nu-plugin/src/serializers/capnp/mod.rs @@ -0,0 +1,43 @@ +mod call; +mod plugin_call; +mod signature; +mod value; + +use nu_protocol::ShellError; + +use crate::{plugin::PluginEncoder, protocol::PluginResponse}; + +#[derive(Clone)] +pub struct CapnpSerializer; + +impl PluginEncoder for CapnpSerializer { + fn encode_call( + &self, + plugin_call: &crate::protocol::PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), nu_protocol::ShellError> { + plugin_call::encode_call(plugin_call, writer) + } + + fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + plugin_call::decode_call(reader) + } + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + plugin_call::encode_response(plugin_response, writer) + } + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + plugin_call::decode_response(reader) + } +} diff --git a/crates/nu-plugin/src/serializers/plugin_call.rs b/crates/nu-plugin/src/serializers/capnp/plugin_call.rs similarity index 98% rename from crates/nu-plugin/src/serializers/plugin_call.rs rename to crates/nu-plugin/src/serializers/capnp/plugin_call.rs index 4216712ffa..3b7e34d5e3 100644 --- a/crates/nu-plugin/src/serializers/plugin_call.rs +++ b/crates/nu-plugin/src/serializers/capnp/plugin_call.rs @@ -1,7 +1,7 @@ -use crate::plugin::{CallInfo, LabeledError, PluginCall, PluginResponse}; +use super::signature::deserialize_signature; +use super::{call, signature, value}; use crate::plugin_capnp::{plugin_call, plugin_response}; -use crate::serializers::signature::deserialize_signature; -use crate::serializers::{call, signature, value}; +use crate::protocol::{CallInfo, LabeledError, PluginCall, PluginResponse}; use capnp::serialize; use nu_protocol::{ShellError, Signature, Span}; @@ -191,8 +191,7 @@ pub fn decode_response(reader: &mut impl std::io::BufRead) -> Result Result<(), nu_protocol::ShellError> { + serde_json::to_writer(writer, plugin_call) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + serde_json::from_reader(reader) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + serde_json::to_writer(writer, plugin_response) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } + + fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + serde_json::from_reader(reader) + .map_err(|err| ShellError::PluginFailedToEncode(err.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::{CallInfo, EvaluatedCall, LabeledError, PluginCall, PluginResponse}; + use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value}; + + #[test] + fn callinfo_round_trip_signature() { + let plugin_call = PluginCall::Signature; + let encoder = JsonSerializer {}; + + let mut buffer: Vec = Vec::new(); + encoder + .encode_call(&plugin_call, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_call(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => {} + PluginCall::CallInfo(_) => panic!("decoded into wrong value"), + } + } + + #[test] + fn callinfo_round_trip_callinfo() { + let name = "test".to_string(); + + let input = Value::Bool { + val: false, + span: Span { start: 1, end: 20 }, + }; + + let call = EvaluatedCall { + head: Span { start: 0, end: 10 }, + positional: vec![ + Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }, + Value::String { + val: "something".into(), + span: Span { start: 0, end: 10 }, + }, + ], + named: vec![( + Spanned { + item: "name".to_string(), + span: Span { start: 0, end: 10 }, + }, + Some(Value::Float { + val: 1.0, + span: Span { start: 0, end: 10 }, + }), + )], + }; + + let plugin_call = PluginCall::CallInfo(Box::new(CallInfo { + name: name.clone(), + call: call.clone(), + input: input.clone(), + })); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_call(&plugin_call, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_call(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginCall::Signature => panic!("returned wrong call type"), + PluginCall::CallInfo(call_info) => { + assert_eq!(name, call_info.name); + assert_eq!(input, call_info.input); + assert_eq!(call.head, call_info.call.head); + assert_eq!(call.positional.len(), call_info.call.positional.len()); + + call.positional + .iter() + .zip(call_info.call.positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + call.named + .iter() + .zip(call_info.call.named.iter()) + .for_each(|(lhs, rhs)| { + // Comparing the keys + assert_eq!(lhs.0.item, rhs.0.item); + + match (&lhs.1, &rhs.1) { + (None, None) => {} + (Some(a), Some(b)) => assert_eq!(a, b), + _ => panic!("not matching values"), + } + }); + } + } + } + + #[test] + fn response_round_trip_signature() { + let signature = Signature::build("nu-plugin") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .required_named("first_named", SyntaxShape::String, "first named", Some('f')) + .required_named( + "second_named", + SyntaxShape::String, + "second named", + Some('s'), + ) + .rest("remaining", SyntaxShape::Int, "remaining"); + + let response = PluginResponse::Signature(vec![signature.clone()]); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + PluginResponse::Signature(returned_signature) => { + assert!(returned_signature.len() == 1); + assert_eq!(signature.name, returned_signature[0].name); + assert_eq!(signature.usage, returned_signature[0].usage); + assert_eq!(signature.extra_usage, returned_signature[0].extra_usage); + assert_eq!(signature.is_filter, returned_signature[0].is_filter); + + signature + .required_positional + .iter() + .zip(returned_signature[0].required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned_signature[0].optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned_signature[0].named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!( + signature.rest_positional, + returned_signature[0].rest_positional, + ); + } + } + } + + #[test] + fn response_round_trip_value() { + let value = Value::Int { + val: 10, + span: Span { start: 2, end: 30 }, + }; + + let response = PluginResponse::Value(Box::new(value.clone())); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(_) => panic!("returned wrong call type"), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(returned_value) => { + assert_eq!(&value, returned_value.as_ref()) + } + } + } + + #[test] + fn response_round_trip_error() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: Some(Span { start: 2, end: 30 }), + }; + let response = PluginResponse::Error(error.clone()); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } + + #[test] + fn response_round_trip_error_none() { + let error = LabeledError { + label: "label".into(), + msg: "msg".into(), + span: None, + }; + let response = PluginResponse::Error(error.clone()); + + let encoder = JsonSerializer {}; + let mut buffer: Vec = Vec::new(); + encoder + .encode_response(&response, &mut buffer) + .expect("unable to serialize message"); + let returned = encoder + .decode_response(&mut buffer.as_slice()) + .expect("unable to deserialize message"); + + match returned { + PluginResponse::Error(msg) => assert_eq!(error, msg), + PluginResponse::Signature(_) => panic!("returned wrong call type"), + PluginResponse::Value(_) => panic!("returned wrong call type"), + } + } +} diff --git a/crates/nu-plugin/src/serializers/mod.rs b/crates/nu-plugin/src/serializers/mod.rs index 0efc99acf9..937732b122 100644 --- a/crates/nu-plugin/src/serializers/mod.rs +++ b/crates/nu-plugin/src/serializers/mod.rs @@ -1,6 +1,74 @@ -mod call; -mod plugin_call; -mod signature; -mod value; +use nu_protocol::ShellError; -pub use plugin_call::*; +use crate::{ + plugin::PluginEncoder, + protocol::{PluginCall, PluginResponse}, +}; + +pub mod capnp; +pub mod json; + +#[derive(Clone)] +pub enum EncodingType { + Capnp(capnp::CapnpSerializer), + Json(json::JsonSerializer), +} + +impl EncodingType { + pub fn try_from_bytes(bytes: &[u8]) -> Option { + match bytes { + b"capnp" => Some(Self::Capnp(capnp::CapnpSerializer {})), + b"json" => Some(Self::Json(json::JsonSerializer {})), + _ => None, + } + } + + pub fn encode_call( + &self, + plugin_call: &PluginCall, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + match self { + EncodingType::Capnp(encoder) => encoder.encode_call(plugin_call, writer), + EncodingType::Json(encoder) => encoder.encode_call(plugin_call, writer), + } + } + + pub fn decode_call( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + match self { + EncodingType::Capnp(encoder) => encoder.decode_call(reader), + EncodingType::Json(encoder) => encoder.decode_call(reader), + } + } + + pub fn encode_response( + &self, + plugin_response: &PluginResponse, + writer: &mut impl std::io::Write, + ) -> Result<(), ShellError> { + match self { + EncodingType::Capnp(encoder) => encoder.encode_response(plugin_response, writer), + EncodingType::Json(encoder) => encoder.encode_response(plugin_response, writer), + } + } + + pub fn decode_response( + &self, + reader: &mut impl std::io::BufRead, + ) -> Result { + match self { + EncodingType::Capnp(encoder) => encoder.decode_response(reader), + EncodingType::Json(encoder) => encoder.decode_response(reader), + } + } + + pub fn to_str(&self) -> &'static str { + match self { + Self::Capnp(_) => "capnp", + Self::Json(_) => "json", + } + } +} diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 65766cd770..21b6b1a231 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -48,8 +48,8 @@ pub trait Command: Send + Sync + CommandClone { self.name().contains(' ') } - // Is a plugin command (returns plugin's name if yes) - fn is_plugin(&self) -> Option<&PathBuf> { + // Is a plugin command (returns plugin's path and encoding if yes) + fn is_plugin(&self) -> Option<(&PathBuf, &str)> { None } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 371ba51e48..9c3717999d 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -230,11 +230,13 @@ impl EngineState { self.plugin_decls().try_for_each(|decl| { // A successful plugin registration already includes the plugin filename // No need to check the None option - let path = decl.is_plugin().expect("plugin should have file name"); + let (path, encoding) = decl.is_plugin().expect("plugin should have file name"); let file_name = path.to_str().expect("path should be a str"); serde_json::to_string_pretty(&decl.signature()) - .map(|signature| format!("register {} {}\n\n", file_name, signature)) + .map(|signature| { + format!("register {} -e {} {}\n\n", file_name, encoding, signature) + }) .map_err(|err| ShellError::PluginFailedToLoad(err.to_string())) .and_then(|line| { plugin_file diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index aa6192c603..cea29479db 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -2,7 +2,7 @@ use miette::SourceSpan; use serde::{Deserialize, Serialize}; /// A spanned area of interest, generic over what kind of thing is of interest -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Spanned where T: Clone + std::fmt::Debug, diff --git a/crates/nu_plugin_example/src/main.rs b/crates/nu_plugin_example/src/main.rs index 96c365bef5..9542d75ad3 100644 --- a/crates/nu_plugin_example/src/main.rs +++ b/crates/nu_plugin_example/src/main.rs @@ -1,6 +1,30 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_example::Example; fn main() { - serve_plugin(&mut Example {}) + // When defining your plugin, you can select the Serializer that could be + // used to encode and decode the messages. The available options are + // CapnpSerializer and JsonSerializer. Both are defined in the serializer + // folder in nu-plugin. + serve_plugin(&mut Example {}, CapnpSerializer {}) + + // Note + // When creating plugins in other languages one needs to consider how a plugin + // is added and used in nushell. + // The steps are: + // - The plugin is register. In this stage nushell calls the binary file of + // the plugin sending information using the encoded PluginCall::Signature object. + // Use this encoded data in your plugin to design the logic that will return + // the encoded signatures. + // Nushell is expecting and encoded PluginResponse::Signature with all the + // plugin signatures + // - When calling the plugin, nushell sends to the binary file the encoded + // PluginCall::CallInfo which has all the call information, such as the + // values of the arguments, the name of the signature called and the input + // from the pipeline. + // Use this data to design your plugin login and to create the value that + // will be sent to nushell + // Nushell expects an encoded PluginResponse::Value from the plugin + // - If an error needs to be sent back to nushell, one can encode PluginResponse::Error. + // This is a labeled error that nushell can format for pretty printing } diff --git a/crates/nu_plugin_example/src/nu/mod.rs b/crates/nu_plugin_example/src/nu/mod.rs index cd40e698c3..7a87e3f840 100644 --- a/crates/nu_plugin_example/src/nu/mod.rs +++ b/crates/nu_plugin_example/src/nu/mod.rs @@ -8,7 +8,7 @@ impl Plugin for Example { // Each signature will be converted to a command declaration once the // plugin is registered to nushell vec![ - Signature::build("test-1") + Signature::build("nu-example-1") .desc("Signature test 1 for plugin. Returns Value::Nothing") .required("a", SyntaxShape::Int, "required integer value") .required("b", SyntaxShape::String, "required string value") @@ -17,7 +17,7 @@ impl Plugin for Example { .named("named", SyntaxShape::String, "named string", Some('n')) .rest("rest", SyntaxShape::String, "rest value string") .category(Category::Experimental), - Signature::build("test-2") + Signature::build("nu-example-2") .desc("Signature test 2 for plugin. Returns list of records") .required("a", SyntaxShape::Int, "required integer value") .required("b", SyntaxShape::String, "required string value") @@ -26,7 +26,7 @@ impl Plugin for Example { .named("named", SyntaxShape::String, "named string", Some('n')) .rest("rest", SyntaxShape::String, "rest value string") .category(Category::Experimental), - Signature::build("test-3") + Signature::build("nu-example-3") .desc("Signature test 3 for plugin. Returns labeled error") .required("a", SyntaxShape::Int, "required integer value") .required("b", SyntaxShape::String, "required string value") @@ -46,12 +46,12 @@ impl Plugin for Example { ) -> Result { // You can use the name to identify what plugin signature was called match name { - "test-1" => self.test1(call, input), - "test-2" => self.test2(call, input), - "test-3" => self.test3(call, input), + "nu-example-1" => self.test1(call, input), + "nu-example-2" => self.test2(call, input), + "nu-example-3" => self.test3(call, input), _ => Err(LabeledError { label: "Plugin call with wrong name signature".into(), - msg: "using the wrong signature".into(), + msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), span: Some(call.head), }), } diff --git a/crates/nu_plugin_gstat/src/main.rs b/crates/nu_plugin_gstat/src/main.rs index b15aeefde5..b8fbc0e55e 100644 --- a/crates/nu_plugin_gstat/src/main.rs +++ b/crates/nu_plugin_gstat/src/main.rs @@ -1,6 +1,6 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_gstat::GStat; fn main() { - serve_plugin(&mut GStat::new()) + serve_plugin(&mut GStat::new(), CapnpSerializer {}) } diff --git a/crates/nu_plugin_inc/src/main.rs b/crates/nu_plugin_inc/src/main.rs index 7245f1fbca..9421c713ce 100644 --- a/crates/nu_plugin_inc/src/main.rs +++ b/crates/nu_plugin_inc/src/main.rs @@ -1,6 +1,6 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_inc::Inc; fn main() { - serve_plugin(&mut Inc::new()) + serve_plugin(&mut Inc::new(), CapnpSerializer {}) } diff --git a/src/plugins/nu_plugin_core_example.rs b/src/plugins/nu_plugin_core_example.rs index 96c365bef5..91e9f7b757 100644 --- a/src/plugins/nu_plugin_core_example.rs +++ b/src/plugins/nu_plugin_core_example.rs @@ -1,6 +1,6 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_example::Example; fn main() { - serve_plugin(&mut Example {}) + serve_plugin(&mut Example {}, CapnpSerializer {}) } diff --git a/src/plugins/nu_plugin_core_inc.rs b/src/plugins/nu_plugin_core_inc.rs index 7245f1fbca..9421c713ce 100644 --- a/src/plugins/nu_plugin_core_inc.rs +++ b/src/plugins/nu_plugin_core_inc.rs @@ -1,6 +1,6 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_inc::Inc; fn main() { - serve_plugin(&mut Inc::new()) + serve_plugin(&mut Inc::new(), CapnpSerializer {}) } diff --git a/src/plugins/nu_plugin_extra_gstat.rs b/src/plugins/nu_plugin_extra_gstat.rs index b15aeefde5..b8fbc0e55e 100644 --- a/src/plugins/nu_plugin_extra_gstat.rs +++ b/src/plugins/nu_plugin_extra_gstat.rs @@ -1,6 +1,6 @@ -use nu_plugin::serve_plugin; +use nu_plugin::{serve_plugin, CapnpSerializer}; use nu_plugin_gstat::GStat; fn main() { - serve_plugin(&mut GStat::new()) + serve_plugin(&mut GStat::new(), CapnpSerializer {}) }