Change PluginCommand API to be more like Command (#12279)

# Description

This is something that was discussed in the core team meeting last
Wednesday. @ayax79 is building `nu-plugin-polars` with all of the
dataframe commands into a plugin, and there are a lot of them, so it
would help to make the API more similar. At the same time, I think the
`Command` API is just better anyway. I don't think the difference is
justified, and the types for core commands have the benefit of requiring
less `.into()` because they often don't own their data

- Broke `signature()` up into `name()`, `usage()`, `extra_usage()`,
`search_terms()`, `examples()`
- `signature()` returns `nu_protocol::Signature`
- `examples()` returns `Vec<nu_protocol::Example>`
- `PluginSignature` and `PluginExample` no longer need to be used by
plugin developers

# User-Facing Changes
Breaking API for plugins yet again 😄
This commit is contained in:
Devyn Cairns
2024-03-27 03:59:57 -07:00
committed by GitHub
parent 03b5e9d853
commit 01d30a416b
45 changed files with 962 additions and 674 deletions

View File

@ -17,8 +17,8 @@
//!
//! ```rust,no_run
//! use nu_plugin::{EvaluatedCall, MsgPackSerializer, serve_plugin};
//! use nu_plugin::{Plugin, PluginCommand, SimplePluginCommand, EngineInterface};
//! use nu_protocol::{PluginSignature, LabeledError, Value};
//! use nu_plugin::{EngineInterface, Plugin, PluginCommand, SimplePluginCommand};
//! use nu_protocol::{LabeledError, Signature, Value};
//!
//! struct MyPlugin;
//! struct MyCommand;
@ -32,7 +32,15 @@
//! impl SimplePluginCommand for MyCommand {
//! type Plugin = MyPlugin;
//!
//! fn signature(&self) -> PluginSignature {
//! fn name(&self) -> &str {
//! "my-command"
//! }
//!
//! fn usage(&self) -> &str {
//! todo!();
//! }
//!
//! fn signature(&self) -> Signature {
//! todo!();
//! }
//!
@ -71,9 +79,10 @@ pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
// Used by other nu crates.
#[doc(hidden)]
pub use plugin::{
get_signature, serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface, InterfaceManager,
PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext, PluginExecutionContext,
PluginInterface, PluginInterfaceManager, PluginSource, ServePluginError,
create_plugin_signature, get_signature, serve_plugin_io, EngineInterfaceManager, GetPlugin,
Interface, InterfaceManager, PersistentPlugin, PluginDeclaration,
PluginExecutionCommandContext, PluginExecutionContext, PluginInterface, PluginInterfaceManager,
PluginSource, ServePluginError,
};
#[doc(hidden)]
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};

View File

@ -1,4 +1,6 @@
use nu_protocol::{LabeledError, PipelineData, PluginSignature, Value};
use nu_protocol::{
Example, LabeledError, PipelineData, PluginExample, PluginSignature, Signature, Value,
};
use crate::{EngineInterface, EvaluatedCall, Plugin};
@ -19,16 +21,23 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value, LabeledError};
/// # use nu_protocol::{Signature, PipelineData, Type, Value, LabeledError};
/// struct LowercasePlugin;
/// struct Lowercase;
///
/// impl PluginCommand for Lowercase {
/// type Plugin = LowercasePlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("lowercase")
/// .usage("Convert each string in a stream to lowercase")
/// fn name(&self) -> &str {
/// "lowercase"
/// }
///
/// fn usage(&self) -> &str {
/// "Convert each string in a stream to lowercase"
/// }
///
/// fn signature(&self) -> Signature {
/// Signature::build(PluginCommand::name(self))
/// .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()))
/// }
///
@ -60,18 +69,62 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// # }
/// ```
pub trait PluginCommand: Sync {
/// The type of plugin this command runs on
/// The type of plugin this command runs on.
///
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
/// plugin that the command expects here.
type Plugin: Plugin;
/// The signature of the plugin command
/// The name of the command from within Nu.
///
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
fn signature(&self) -> PluginSignature;
/// In case this contains spaces, it will be treated as a subcommand.
fn name(&self) -> &str;
/// Perform the actual behavior of the plugin command
/// The signature of the command.
///
/// This defines the arguments and input/output types of the command.
fn signature(&self) -> Signature;
/// A brief description of usage for the command.
///
/// This should be short enough to fit in completion menus.
fn usage(&self) -> &str;
/// Additional documentation for usage of the command.
///
/// This is optional - any arguments documented by [`.signature()`] will be shown in the help
/// page automatically. However, this can be useful for explaining things that would be too
/// brief to include in [`.usage()`] and may span multiple lines.
fn extra_usage(&self) -> &str {
""
}
/// Search terms to help users find the command.
///
/// A search query matching any of these search keywords, e.g. on `help --find`, will also
/// show this command as a result. This may be used to suggest this command as a replacement
/// for common system commands, or based alternate names for the functionality this command
/// provides.
///
/// For example, a `fold` command might mention `reduce` in its search terms.
fn search_terms(&self) -> Vec<&str> {
vec![]
}
/// Examples, in Nu, of how the command might be used.
///
/// The examples are not restricted to only including this command, and may demonstrate
/// pipelines using the command. A `result` may optionally be provided to show users what the
/// command would return.
///
/// `PluginTest::test_command_examples()` from the
/// [`nu-plugin-test-support`](https://docs.rs/nu-plugin-test-support) crate can be used in
/// plugin tests to automatically test that examples produce the `result`s as specified.
fn examples(&self) -> Vec<Example> {
vec![]
}
/// Perform the actual behavior of the plugin command.
///
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
/// invoked the plugin [`serve_plugin`](crate::serve_plugin) will call this method and print the
@ -109,15 +162,23 @@ pub trait PluginCommand: Sync {
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, Type, Value, LabeledError};
/// # use nu_protocol::{LabeledError, Signature, Type, Value};
/// struct HelloPlugin;
/// struct Hello;
///
/// impl SimplePluginCommand for Hello {
/// type Plugin = HelloPlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("hello")
/// fn name(&self) -> &str {
/// "hello"
/// }
///
/// fn usage(&self) -> &str {
/// "Every programmer's favorite greeting"
/// }
///
/// fn signature(&self) -> Signature {
/// Signature::build(PluginCommand::name(self))
/// .input_output_type(Type::Nothing, Type::String)
/// }
///
@ -143,18 +204,62 @@ pub trait PluginCommand: Sync {
/// # }
/// ```
pub trait SimplePluginCommand: Sync {
/// The type of plugin this command runs on
/// The type of plugin this command runs on.
///
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
/// plugin that the command expects here.
type Plugin: Plugin;
/// The signature of the plugin command
/// The name of the command from within Nu.
///
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
fn signature(&self) -> PluginSignature;
/// In case this contains spaces, it will be treated as a subcommand.
fn name(&self) -> &str;
/// Perform the actual behavior of the plugin command
/// The signature of the command.
///
/// This defines the arguments and input/output types of the command.
fn signature(&self) -> Signature;
/// A brief description of usage for the command.
///
/// This should be short enough to fit in completion menus.
fn usage(&self) -> &str;
/// Additional documentation for usage of the command.
///
/// This is optional - any arguments documented by [`.signature()`] will be shown in the help
/// page automatically. However, this can be useful for explaining things that would be too
/// brief to include in [`.usage()`] and may span multiple lines.
fn extra_usage(&self) -> &str {
""
}
/// Search terms to help users find the command.
///
/// A search query matching any of these search keywords, e.g. on `help --find`, will also
/// show this command as a result. This may be used to suggest this command as a replacement
/// for common system commands, or based alternate names for the functionality this command
/// provides.
///
/// For example, a `fold` command might mention `reduce` in its search terms.
fn search_terms(&self) -> Vec<&str> {
vec![]
}
/// Examples, in Nu, of how the command might be used.
///
/// The examples are not restricted to only including this command, and may demonstrate
/// pipelines using the command. A `result` may optionally be provided to show users what the
/// command would return.
///
/// `PluginTest::test_command_examples()` from the
/// [`nu-plugin-test-support`](https://docs.rs/nu-plugin-test-support) crate can be used in
/// plugin tests to automatically test that examples produce the `result`s as specified.
fn examples(&self) -> Vec<Example> {
vec![]
}
/// Perform the actual behavior of the plugin command.
///
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
/// invoked the plugin [`serve_plugin`](crate::serve_plugin) will call this method and print the
@ -185,8 +290,16 @@ where
{
type Plugin = <Self as SimplePluginCommand>::Plugin;
fn signature(&self) -> PluginSignature {
<Self as SimplePluginCommand>::signature(self)
fn examples(&self) -> Vec<Example> {
<Self as SimplePluginCommand>::examples(self)
}
fn extra_usage(&self) -> &str {
<Self as SimplePluginCommand>::extra_usage(self)
}
fn name(&self) -> &str {
<Self as SimplePluginCommand>::name(self)
}
fn run(
@ -204,4 +317,45 @@ where
<Self as SimplePluginCommand>::run(self, plugin, engine, call, &input_value)
.map(|value| PipelineData::Value(value, None))
}
fn search_terms(&self) -> Vec<&str> {
<Self as SimplePluginCommand>::search_terms(self)
}
fn signature(&self) -> Signature {
<Self as SimplePluginCommand>::signature(self)
}
fn usage(&self) -> &str {
<Self as SimplePluginCommand>::usage(self)
}
}
/// Build a [`PluginSignature`] from the signature-related methods on [`PluginCommand`].
///
/// This is sent to the engine on `register`.
///
/// This is not a public API.
#[doc(hidden)]
pub fn create_plugin_signature(command: &(impl PluginCommand + ?Sized)) -> PluginSignature {
PluginSignature::new(
// Add results of trait methods to signature
command
.signature()
.usage(command.usage())
.extra_usage(command.extra_usage())
.search_terms(
command
.search_terms()
.into_iter()
.map(String::from)
.collect(),
),
// Convert `Example`s to `PluginExample`s
command
.examples()
.into_iter()
.map(PluginExample::from)
.collect(),
)
}

View File

@ -11,7 +11,7 @@ use crate::{
};
use nu_protocol::{
engine::Closure, Config, CustomValue, IntoInterruptiblePipelineData, LabeledError,
PipelineData, PluginExample, PluginSignature, ShellError, Span, Spanned, Value,
PipelineData, PluginExample, PluginSignature, ShellError, Signature, Span, Spanned, Value,
};
use std::{
collections::HashMap,
@ -786,15 +786,16 @@ fn interface_write_signature() -> Result<(), ShellError> {
fn interface_write_signature_custom_value() -> Result<(), ShellError> {
let test = TestCase::new();
let interface = test.engine().interface_for_context(38);
let signatures = vec![PluginSignature::build("test command").plugin_examples(vec![
PluginExample {
let signatures = vec![PluginSignature::new(
Signature::build("test command"),
vec![PluginExample {
example: "test command".into(),
description: "a test".into(),
result: Some(Value::test_custom_value(Box::new(
expected_test_custom_value(),
))),
},
])];
}],
)];
interface.write_signature(signatures.clone())?;
let written = test.next_written().expect("nothing written");

View File

@ -3,15 +3,7 @@ use crate::{
protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput},
EncodingType,
};
use nu_engine::documentation::get_flags_section;
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginSignature,
ShellError, Spanned, Value,
};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{
cmp::Ordering,
collections::HashMap,
@ -19,14 +11,28 @@ use std::{
ffi::OsStr,
fmt::Write,
io::{BufReader, Read, Write as WriteTrait},
ops::Deref,
path::Path,
process::{Child, ChildStdout, Command as CommandSys, Stdio},
sync::mpsc::TrySendError,
sync::{mpsc, Arc, Mutex},
sync::{
mpsc::{self, TrySendError},
Arc, Mutex,
},
thread,
};
use nu_engine::documentation::get_flags_section;
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginSignature,
ShellError, Spanned, Value,
};
use thiserror::Error;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use self::gc::PluginGc;
pub use self::interface::{PluginRead, PluginWrite};
@ -38,7 +44,7 @@ mod interface;
mod persistent;
mod source;
pub use command::{PluginCommand, SimplePluginCommand};
pub use command::{create_plugin_signature, PluginCommand, SimplePluginCommand};
pub use declaration::PluginDeclaration;
pub use interface::{
EngineInterface, EngineInterfaceManager, Interface, InterfaceManager, PluginInterface,
@ -229,7 +235,7 @@ where
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, LabeledError, Type, Value};
/// # use nu_protocol::{LabeledError, Signature, Type, Value};
/// struct HelloPlugin;
/// struct Hello;
///
@ -242,8 +248,16 @@ where
/// impl SimplePluginCommand for Hello {
/// type Plugin = HelloPlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("hello")
/// fn name(&self) -> &str {
/// "hello"
/// }
///
/// fn usage(&self) -> &str {
/// "Every programmer's favorite greeting"
/// }
///
/// fn signature(&self) -> Signature {
/// Signature::build(PluginCommand::name(self))
/// .input_output_type(Type::Nothing, Type::String)
/// }
///
@ -556,11 +570,11 @@ where
let mut commands: HashMap<String, _> = HashMap::new();
for command in plugin.commands() {
if let Some(previous) = commands.insert(command.signature().sig.name.clone(), command) {
if let Some(previous) = commands.insert(command.name().into(), command) {
eprintln!(
"Plugin `{plugin_name}` warning: command `{}` shadowed by another command with the \
same name. Check your command signatures",
previous.signature().sig.name
same name. Check your commands' `name()` methods",
previous.name()
);
}
}
@ -636,7 +650,7 @@ where
ReceivedPluginCall::Signature { engine } => {
let sigs = commands
.values()
.map(|command| command.signature())
.map(|command| create_plugin_signature(command.deref()))
.collect();
engine.write_signature(sigs).try_to_report(&engine)?;
}
@ -752,23 +766,22 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
plugin.commands().into_iter().for_each(|command| {
let signature = command.signature();
let res = write!(help, "\nCommand: {}", signature.sig.name)
.and_then(|_| writeln!(help, "\nUsage:\n > {}", signature.sig.usage))
let res = write!(help, "\nCommand: {}", command.name())
.and_then(|_| writeln!(help, "\nUsage:\n > {}", command.usage()))
.and_then(|_| {
if !signature.sig.extra_usage.is_empty() {
writeln!(help, "\nExtra usage:\n > {}", signature.sig.extra_usage)
if !command.extra_usage().is_empty() {
writeln!(help, "\nExtra usage:\n > {}", command.extra_usage())
} else {
Ok(())
}
})
.and_then(|_| {
let flags = get_flags_section(None, &signature.sig, |v| format!("{:#?}", v));
let flags = get_flags_section(None, &signature, |v| format!("{:#?}", v));
write!(help, "{flags}")
})
.and_then(|_| writeln!(help, "\nParameters:"))
.and_then(|_| {
signature
.sig
.required_positional
.iter()
.try_for_each(|positional| {
@ -781,7 +794,6 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
})
.and_then(|_| {
signature
.sig
.optional_positional
.iter()
.try_for_each(|positional| {
@ -793,7 +805,7 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
})
})
.and_then(|_| {
if let Some(rest_positional) = &signature.sig.rest_positional {
if let Some(rest_positional) = &signature.rest_positional {
writeln!(
help,
" ...{} <{}>: {}",

View File

@ -5,7 +5,9 @@ macro_rules! generate_tests {
PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
StreamData, StreamMessage,
};
use nu_protocol::{LabeledError, PluginSignature, Span, Spanned, SyntaxShape, Value};
use nu_protocol::{
LabeledError, PluginSignature, Signature, Span, Spanned, SyntaxShape, Value,
};
#[test]
fn decode_eof() {
@ -211,17 +213,20 @@ macro_rules! generate_tests {
#[test]
fn response_round_trip_signature() {
let signature = PluginSignature::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 signature = PluginSignature::new(
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"),
vec![],
);
let response = PluginCallResponse::Signature(vec![signature.clone()]);
let output = PluginOutput::CallResponse(3, response);