mirror of
https://github.com/nushell/nushell.git
synced 2025-01-03 13:00:08 +01:00
Add CustomValue support to plugins (#6070)
* Skeleton implementation Lots and lots of TODOs * Bootstrap simple CustomValue plugin support test * Create nu_plugin_custom_value * Skeleton for nu_plugin_custom_values * Return a custom value from plugin * Encode CustomValues from plugin calls as PluginResponse::PluginData * Add new PluginCall variant CollapseCustomValue * Handle CollapseCustomValue plugin calls * Add CallInput::Data variant to CallInfo inputs * Handle CallInfo with CallInput::Data plugin calls * Send CallInput::Data if Value is PluginCustomValue from plugin calls * Remove unnecessary boxing of plugins CallInfo * Add fields needed to collapse PluginCustomValue to it * Document PluginCustomValue and its purpose * Impl collapsing using plugin calls in PluginCustomValue::to_base_value * Implement proper typetag based deserialization for CoolCustomValue * Test demonstrating that passing back a custom value to plugin works * Added a failing test for describing plugin CustomValues * Support describe for PluginCustomValues - Add name to PluginResponse::PluginData - Also turn it into a struct for clarity - Add name to PluginCustomValue - Return name field from PluginCustomValue * Demonstrate that plugins can create and handle multiple CustomValues * Add bincode to nu-plugin dependencies This is for demonstration purposes, any schemaless binary seralization format will work. I picked bincode since it's the most popular for Rust but there are defintely better options out there for this usecase * serde_json::Value -> Vec<u8> * Update capnp schema for new CallInfo.input field * Move call_input capnp serialization and deserialization into new file * Deserialize Value's span from Value itself instead of passing call.head I am not sure if this was correct and I am breaking it or if it was a bug, I don't fully understand how nu creates and uses Spans. What should reuse spans and what should recreate new ones? But yeah it felt weird that the Value's Span was being ignored since in the json serializer just uses the Value's Span * Add call_info value round trip test * Add capnp CallInput::Data serialization and deserialization support * Add CallInfo::CollapseCustomValue to capnp schema * Add capnp PluginCall::CollapseCustomValue serialization and deserialization support * Add PluginResponse::PluginData to capnp schema * Add capnp PluginResponse::PluginData serialization and deserialization support * Switch plugins::custom_values tests to capnp Both json and capnp would work now! Sadly I can't choose both at the same time :( * Add missing JsonSerializer round trip tests * Handle plugin returning PluginData as a response to CollapseCustomValue * Refactor plugin calling into a reusable function Many less levels of indentation now! * Export PluginData from nu_plugin So plugins can create their very own serve_plugin with whatever CustomValue behavior they may desire * Error if CustomValue cannot be handled by Plugin
This commit is contained in:
parent
9097e865ca
commit
daa2148136
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -271,6 +271,15 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.59.2"
|
||||
@ -2720,6 +2729,7 @@ dependencies = [
|
||||
name = "nu-plugin"
|
||||
version = "0.65.1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"capnp",
|
||||
"nu-engine",
|
||||
"nu-protocol",
|
||||
@ -2809,6 +2819,16 @@ dependencies = [
|
||||
"lscolors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu_plugin_custom_values"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nu-plugin",
|
||||
"nu-protocol",
|
||||
"serde",
|
||||
"typetag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu_plugin_example"
|
||||
version = "0.65.1"
|
||||
|
@ -28,6 +28,7 @@ members = [
|
||||
"crates/nu_plugin_gstat",
|
||||
"crates/nu_plugin_example",
|
||||
"crates/nu_plugin_query",
|
||||
"crates/nu_plugin_custom_values",
|
||||
"crates/nu-utils",
|
||||
]
|
||||
|
||||
|
@ -7,6 +7,7 @@ name = "nu-plugin"
|
||||
version = "0.65.1"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
capnp = "0.14.3"
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.65.1" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.65.1" }
|
||||
|
@ -6,5 +6,5 @@ mod serializers;
|
||||
mod plugin_capnp;
|
||||
|
||||
pub use plugin::{get_signature, serve_plugin, Plugin, PluginDeclaration};
|
||||
pub use protocol::{EvaluatedCall, LabeledError};
|
||||
pub use protocol::{EvaluatedCall, LabeledError, PluginData};
|
||||
pub use serializers::{capnp::CapnpSerializer, json::JsonSerializer, EncodingType};
|
||||
|
@ -1,13 +1,14 @@
|
||||
use crate::{EncodingType, EvaluatedCall};
|
||||
|
||||
use super::{create_command, OUTPUT_BUFFER_SIZE};
|
||||
use crate::protocol::{CallInfo, PluginCall, PluginResponse};
|
||||
use std::io::BufReader;
|
||||
use super::{call_plugin, create_command};
|
||||
use crate::protocol::{
|
||||
CallInfo, CallInput, PluginCall, PluginCustomValue, PluginData, PluginResponse,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{ast::Call, Signature};
|
||||
use nu_protocol::{PipelineData, ShellError};
|
||||
use nu_protocol::{PipelineData, ShellError, Value};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginDeclaration {
|
||||
@ -73,28 +74,41 @@ impl Command for PluginDeclaration {
|
||||
})?;
|
||||
|
||||
let input = input.into_value(call.head);
|
||||
let input = match input {
|
||||
Value::CustomValue { val, span } => {
|
||||
match val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
Some(plugin_data) if plugin_data.filename == self.filename => {
|
||||
CallInput::Data(PluginData {
|
||||
data: plugin_data.data.clone(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let custom_value_name = val.value_string();
|
||||
return Err(ShellError::GenericError(
|
||||
format!(
|
||||
"Plugin {} can not handle the custom value {}",
|
||||
self.name, custom_value_name
|
||||
),
|
||||
format!("custom value {}", custom_value_name),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
value => CallInput::Value(value),
|
||||
};
|
||||
|
||||
// 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 {
|
||||
let plugin_call = PluginCall::CallInfo(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 response =
|
||||
call_plugin(&mut child, plugin_call, &self.encoding, call.head).map_err(|err| {
|
||||
let decl = engine_state.get_decl(call.decl_id);
|
||||
ShellError::GenericError(
|
||||
format!("Unable to decode call for {}", decl.name()),
|
||||
@ -105,10 +119,24 @@ impl Command for PluginDeclaration {
|
||||
)
|
||||
});
|
||||
|
||||
match response {
|
||||
let pipeline_data = match response {
|
||||
Ok(PluginResponse::Value(value)) => {
|
||||
Ok(PipelineData::Value(value.as_ref().clone(), None))
|
||||
}
|
||||
Ok(PluginResponse::PluginData(name, plugin_data)) => Ok(PipelineData::Value(
|
||||
Value::CustomValue {
|
||||
val: Box::new(PluginCustomValue {
|
||||
name,
|
||||
data: plugin_data.data,
|
||||
filename: self.filename.clone(),
|
||||
shell: self.shell.clone(),
|
||||
encoding: self.encoding.clone(),
|
||||
source: engine_state.get_decl(call.decl_id).name().to_owned(),
|
||||
}),
|
||||
span: plugin_data.span,
|
||||
},
|
||||
None,
|
||||
)),
|
||||
Ok(PluginResponse::Error(err)) => Err(err.into()),
|
||||
Ok(PluginResponse::Signature(..)) => Err(ShellError::GenericError(
|
||||
"Plugin missing value".into(),
|
||||
@ -118,15 +146,6 @@ impl Command for PluginDeclaration {
|
||||
Vec::new(),
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
} else {
|
||||
Err(ShellError::GenericError(
|
||||
"Error with stdout reader".into(),
|
||||
"no stdout reader".into(),
|
||||
Some(call.head),
|
||||
None,
|
||||
Vec::new(),
|
||||
))
|
||||
};
|
||||
|
||||
// We need to call .wait() on the child, or we'll risk summoning the zombie horde
|
||||
|
@ -1,18 +1,18 @@
|
||||
mod declaration;
|
||||
pub use declaration::PluginDeclaration;
|
||||
|
||||
use crate::protocol::{LabeledError, PluginCall, PluginResponse};
|
||||
use crate::protocol::{CallInput, LabeledError, PluginCall, PluginData, PluginResponse};
|
||||
use crate::EncodingType;
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command as CommandSys, Stdio};
|
||||
use std::process::{Child, Command as CommandSys, Stdio};
|
||||
|
||||
use nu_protocol::ShellError;
|
||||
use nu_protocol::{CustomValue, ShellError, Span};
|
||||
use nu_protocol::{Signature, Value};
|
||||
|
||||
use super::EvaluatedCall;
|
||||
|
||||
const OUTPUT_BUFFER_SIZE: usize = 8192;
|
||||
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
|
||||
|
||||
pub trait PluginEncoder: Clone {
|
||||
fn encode_call(
|
||||
@ -35,7 +35,7 @@ pub trait PluginEncoder: Clone {
|
||||
) -> Result<PluginResponse, ShellError>;
|
||||
}
|
||||
|
||||
fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
|
||||
pub(crate) fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
|
||||
let mut process = match (path.extension(), shell) {
|
||||
(_, Some(shell)) => {
|
||||
let mut process = std::process::Command::new(shell);
|
||||
@ -77,6 +77,37 @@ fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
|
||||
process
|
||||
}
|
||||
|
||||
pub(crate) fn call_plugin(
|
||||
child: &mut Child,
|
||||
plugin_call: PluginCall,
|
||||
encoding: &EncodingType,
|
||||
span: Span,
|
||||
) -> Result<PluginResponse, ShellError> {
|
||||
if let Some(mut stdin_writer) = child.stdin.take() {
|
||||
let encoding_clone = encoding.clone();
|
||||
std::thread::spawn(move || {
|
||||
// PluginCall information
|
||||
encoding_clone.encode_call(&plugin_call, &mut stdin_writer)
|
||||
});
|
||||
}
|
||||
|
||||
// Deserialize response from plugin to extract the resulting value
|
||||
if let Some(stdout_reader) = &mut child.stdout {
|
||||
let reader = stdout_reader;
|
||||
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
|
||||
|
||||
encoding.decode_response(&mut buf_read)
|
||||
} else {
|
||||
Err(ShellError::GenericError(
|
||||
"Error with stdout reader".into(),
|
||||
"no stdout reader".into(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_signature(
|
||||
path: &Path,
|
||||
encoding: &EncodingType,
|
||||
@ -172,9 +203,33 @@ pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
|
||||
.expect("Error encoding response");
|
||||
}
|
||||
PluginCall::CallInfo(call_info) => {
|
||||
let value = plugin.run(&call_info.name, &call_info.call, &call_info.input);
|
||||
let input = match call_info.input {
|
||||
CallInput::Value(value) => Ok(value),
|
||||
CallInput::Data(plugin_data) => {
|
||||
bincode::deserialize::<Box<dyn CustomValue>>(&plugin_data.data)
|
||||
.map(|custom_value| Value::CustomValue {
|
||||
val: custom_value,
|
||||
span: plugin_data.span,
|
||||
})
|
||||
.map_err(|err| ShellError::PluginFailedToDecode(err.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let value = match input {
|
||||
Ok(input) => plugin.run(&call_info.name, &call_info.call, &input),
|
||||
Err(err) => Err(err.into()),
|
||||
};
|
||||
|
||||
let response = match value {
|
||||
Ok(Value::CustomValue { val, span }) => match bincode::serialize(&val) {
|
||||
Ok(data) => {
|
||||
let name = val.value_string();
|
||||
PluginResponse::PluginData(name, PluginData { data, span })
|
||||
}
|
||||
Err(err) => PluginResponse::Error(
|
||||
ShellError::PluginFailedToEncode(err.to_string()).into(),
|
||||
),
|
||||
},
|
||||
Ok(value) => PluginResponse::Value(Box::new(value)),
|
||||
Err(err) => PluginResponse::Error(err),
|
||||
};
|
||||
@ -182,6 +237,18 @@ pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
|
||||
.encode_response(&response, &mut std::io::stdout())
|
||||
.expect("Error encoding response");
|
||||
}
|
||||
PluginCall::CollapseCustomValue(plugin_data) => {
|
||||
let response = bincode::deserialize::<Box<dyn CustomValue>>(&plugin_data.data)
|
||||
.map_err(|err| ShellError::PluginFailedToDecode(err.to_string()))
|
||||
.and_then(|val| val.to_base_value(plugin_data.span))
|
||||
.map(Box::new)
|
||||
.map_err(LabeledError::from)
|
||||
.map_or_else(PluginResponse::Error, PluginResponse::Value);
|
||||
|
||||
encoder
|
||||
.encode_response(&response, &mut std::io::stdout())
|
||||
.expect("Error encoding response");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,32 @@
|
||||
mod evaluated_call;
|
||||
mod plugin_custom_value;
|
||||
mod plugin_data;
|
||||
|
||||
pub use evaluated_call::EvaluatedCall;
|
||||
use nu_protocol::{ShellError, Signature, Span, Value};
|
||||
pub use plugin_custom_value::PluginCustomValue;
|
||||
pub use plugin_data::PluginData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CallInfo {
|
||||
pub name: String,
|
||||
pub call: EvaluatedCall,
|
||||
pub input: Value,
|
||||
pub input: CallInput,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum CallInput {
|
||||
Value(Value),
|
||||
Data(PluginData),
|
||||
}
|
||||
|
||||
// Information sent to the plugin
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum PluginCall {
|
||||
Signature,
|
||||
CallInfo(Box<CallInfo>),
|
||||
CallInfo(CallInfo),
|
||||
CollapseCustomValue(PluginData),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
@ -88,4 +99,5 @@ pub enum PluginResponse {
|
||||
Error(LabeledError),
|
||||
Signature(Vec<Signature>),
|
||||
Value(Box<Value>),
|
||||
PluginData(String, PluginData),
|
||||
}
|
||||
|
126
crates/nu-plugin/src/protocol/plugin_custom_value.rs
Normal file
126
crates/nu-plugin/src/protocol/plugin_custom_value.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use nu_protocol::{CustomValue, ShellError, Value};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
plugin::{call_plugin, create_command},
|
||||
EncodingType,
|
||||
};
|
||||
|
||||
use super::{PluginCall, PluginData, PluginResponse};
|
||||
|
||||
/// An opaque container for a custom value that is handled fully by a plugin
|
||||
///
|
||||
/// This is constructed by the main nushell engine when it receives [`PluginResponse::PluginData`]
|
||||
/// it stores that data as well as metadata related to the plugin to be able to call the plugin
|
||||
/// later.
|
||||
/// Since the data in it is opaque to the engine, there are only two final destinations for it:
|
||||
/// either it will be sent back to the plugin that generated it across a pipeline, or it will be
|
||||
/// sent to the plugin with a request to collapse it into a base value
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct PluginCustomValue {
|
||||
/// The name of the custom value as defined by the plugin
|
||||
pub name: String,
|
||||
pub data: Vec<u8>,
|
||||
pub filename: PathBuf,
|
||||
|
||||
// PluginCustomValue must implement Serialize because all CustomValues must implement Serialize
|
||||
// However, the main place where values are serialized and deserialized is when they are being
|
||||
// sent between plugins and nushell's main engine. PluginCustomValue is never meant to be sent
|
||||
// between that boundary
|
||||
#[serde(skip)]
|
||||
pub shell: Option<PathBuf>,
|
||||
#[serde(skip)]
|
||||
pub encoding: EncodingType,
|
||||
#[serde(skip)]
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
impl CustomValue for PluginCustomValue {
|
||||
fn clone_value(&self, span: nu_protocol::Span) -> nu_protocol::Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self.clone()),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn to_base_value(
|
||||
&self,
|
||||
span: nu_protocol::Span,
|
||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||
let mut plugin_cmd = create_command(&self.filename, &self.shell);
|
||||
|
||||
let mut child = plugin_cmd.spawn().map_err(|err| {
|
||||
ShellError::GenericError(
|
||||
format!(
|
||||
"Unable to spawn plugin for {} to get base value",
|
||||
self.source
|
||||
),
|
||||
format!("{}", err),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let plugin_call = PluginCall::CollapseCustomValue(PluginData {
|
||||
data: self.data.clone(),
|
||||
span,
|
||||
});
|
||||
|
||||
let response = call_plugin(&mut child, plugin_call, &self.encoding, span).map_err(|err| {
|
||||
ShellError::GenericError(
|
||||
format!(
|
||||
"Unable to decode call for {} to get base value",
|
||||
self.source
|
||||
),
|
||||
format!("{}", err),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
});
|
||||
|
||||
let value = match response {
|
||||
Ok(PluginResponse::Value(value)) => Ok(*value),
|
||||
Ok(PluginResponse::PluginData(..)) => Err(ShellError::GenericError(
|
||||
"Plugin misbehaving".into(),
|
||||
"Plugin returned custom data as a response to a collapse call".into(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)),
|
||||
Ok(PluginResponse::Error(err)) => Err(err.into()),
|
||||
Ok(PluginResponse::Signature(..)) => Err(ShellError::GenericError(
|
||||
"Plugin missing value".into(),
|
||||
"Received a signature from plugin instead of value".into(),
|
||||
Some(span),
|
||||
None,
|
||||
Vec::new(),
|
||||
)),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
// We need to call .wait() on the child, or we'll risk summoning the zombie horde
|
||||
let _ = child.wait();
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
"PluginCustomValue"
|
||||
}
|
||||
|
||||
fn typetag_deserialize(&self) {
|
||||
unimplemented!("typetag_deserialize")
|
||||
}
|
||||
}
|
8
crates/nu-plugin/src/protocol/plugin_data.rs
Normal file
8
crates/nu-plugin/src/protocol/plugin_data.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use nu_protocol::Span;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct PluginData {
|
||||
pub data: Vec<u8>,
|
||||
pub span: Span,
|
||||
}
|
113
crates/nu-plugin/src/serializers/capnp/call_input.rs
Normal file
113
crates/nu-plugin/src/serializers/capnp/call_input.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use super::{plugin_data, value};
|
||||
use crate::{plugin_capnp::call_input, protocol::CallInput};
|
||||
use nu_protocol::{ShellError, Span};
|
||||
|
||||
pub(crate) fn serialize_call_input(call_input: &CallInput, builder: call_input::Builder) {
|
||||
match call_input {
|
||||
CallInput::Value(value) => {
|
||||
value::serialize_value(value, builder.init_value());
|
||||
}
|
||||
CallInput::Data(plugin_data) => {
|
||||
let builder = builder.init_plugin_data();
|
||||
|
||||
plugin_data::serialize_plugin_data(plugin_data, builder);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_call_input(reader: call_input::Reader) -> Result<CallInput, ShellError> {
|
||||
match reader.which() {
|
||||
Err(capnp::NotInSchema(_)) => Err(ShellError::PluginFailedToDecode(
|
||||
"value not in schema".into(),
|
||||
)),
|
||||
Ok(call_input::Value(value_reader)) => {
|
||||
let value_reader =
|
||||
value_reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let span_reader = value_reader
|
||||
.get_span()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let span = Span {
|
||||
start: span_reader.get_start() as usize,
|
||||
end: span_reader.get_end() as usize,
|
||||
};
|
||||
|
||||
Ok(CallInput::Value(value::deserialize_value(
|
||||
value_reader,
|
||||
span,
|
||||
)?))
|
||||
}
|
||||
Ok(call_input::PluginData(plugin_data_reader)) => {
|
||||
let plugin_data_reader =
|
||||
plugin_data_reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let plugin_data = plugin_data::deserialize_plugin_data(plugin_data_reader)?;
|
||||
|
||||
Ok(CallInput::Data(plugin_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::{CallInput, PluginData};
|
||||
use capnp::serialize;
|
||||
use nu_protocol::{Span, Value};
|
||||
|
||||
pub fn write_buffer(
|
||||
call_input: &CallInput,
|
||||
writer: &mut impl std::io::Write,
|
||||
) -> Result<(), ShellError> {
|
||||
let mut message = ::capnp::message::Builder::new_default();
|
||||
|
||||
let mut builder = message.init_root::<call_input::Builder>();
|
||||
|
||||
serialize_call_input(call_input, builder.reborrow());
|
||||
|
||||
serialize::write_message(writer, &message)
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn read_buffer(reader: &mut impl std::io::BufRead) -> Result<CallInput, ShellError> {
|
||||
let message_reader =
|
||||
serialize::read_message(reader, ::capnp::message::ReaderOptions::new()).unwrap();
|
||||
|
||||
let reader = message_reader
|
||||
.get_root::<call_input::Reader>()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
deserialize_call_input(reader.reborrow())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callinput_value_round_trip() {
|
||||
let call_input = CallInput::Value(Value::String {
|
||||
val: "abc".to_string(),
|
||||
span: Span { start: 1, end: 20 },
|
||||
});
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
write_buffer(&call_input, &mut buffer).expect("unable to serialize message");
|
||||
let returned_call_input =
|
||||
read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||
|
||||
assert_eq!(call_input, returned_call_input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callinput_data_round_trip() {
|
||||
let call_input = CallInput::Data(PluginData {
|
||||
data: vec![1, 2, 3, 4, 5, 6, 7],
|
||||
span: Span { start: 1, end: 20 },
|
||||
});
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
write_buffer(&call_input, &mut buffer).expect("unable to serialize message");
|
||||
let returned_call_input =
|
||||
read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||
|
||||
assert_eq!(call_input, returned_call_input)
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
mod call;
|
||||
mod call_input;
|
||||
mod plugin_call;
|
||||
mod plugin_data;
|
||||
mod signature;
|
||||
mod value;
|
||||
|
||||
@ -7,7 +9,7 @@ use nu_protocol::ShellError;
|
||||
|
||||
use crate::{plugin::PluginEncoder, protocol::PluginResponse};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CapnpSerializer;
|
||||
|
||||
impl PluginEncoder for CapnpSerializer {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::signature::deserialize_signature;
|
||||
use super::{call, signature, value};
|
||||
use super::{call, call_input, plugin_data, signature, value};
|
||||
use crate::plugin_capnp::{plugin_call, plugin_response};
|
||||
use crate::protocol::{CallInfo, LabeledError, PluginCall, PluginResponse};
|
||||
use capnp::serialize;
|
||||
@ -31,12 +31,17 @@ pub fn encode_call(
|
||||
.map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?;
|
||||
|
||||
// Serializing the input value from the call info
|
||||
let value_builder = call_info_builder
|
||||
let call_input_builder = call_info_builder
|
||||
.reborrow()
|
||||
.get_input()
|
||||
.map_err(|e| ShellError::PluginFailedToEncode(e.to_string()))?;
|
||||
|
||||
value::serialize_value(&call_info.input, value_builder);
|
||||
call_input::serialize_call_input(&call_info.input, call_input_builder);
|
||||
}
|
||||
PluginCall::CollapseCustomValue(plugin_data) => {
|
||||
let builder = builder.init_collapse_custom_value();
|
||||
|
||||
plugin_data::serialize_plugin_data(plugin_data, builder);
|
||||
}
|
||||
};
|
||||
|
||||
@ -74,13 +79,20 @@ pub fn decode_call(reader: &mut impl std::io::BufRead) -> Result<PluginCall, She
|
||||
.get_input()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let input = value::deserialize_value(input_reader, call.head)?;
|
||||
let input = call_input::deserialize_call_input(input_reader)?;
|
||||
|
||||
Ok(PluginCall::CallInfo(Box::new(CallInfo {
|
||||
Ok(PluginCall::CallInfo(CallInfo {
|
||||
name: name.to_string(),
|
||||
call,
|
||||
input,
|
||||
})))
|
||||
}))
|
||||
}
|
||||
Ok(plugin_call::CollapseCustomValue(reader)) => {
|
||||
let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let plugin_data = plugin_data::deserialize_plugin_data(reader)?;
|
||||
|
||||
Ok(PluginCall::CollapseCustomValue(plugin_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -118,6 +130,12 @@ pub fn encode_response(
|
||||
let value_builder = builder.reborrow().init_value();
|
||||
value::serialize_value(val, value_builder);
|
||||
}
|
||||
PluginResponse::PluginData(name, plugin_data) => {
|
||||
let mut plugin_data_builder = builder.reborrow().init_plugin_data();
|
||||
|
||||
plugin_data_builder.set_name(name);
|
||||
plugin_data::serialize_plugin_data(plugin_data, plugin_data_builder.init_data());
|
||||
}
|
||||
};
|
||||
|
||||
serialize::write_message(writer, &message)
|
||||
@ -196,13 +214,29 @@ pub fn decode_response(reader: &mut impl std::io::BufRead) -> Result<PluginRespo
|
||||
|
||||
Ok(PluginResponse::Value(Box::new(val)))
|
||||
}
|
||||
Ok(plugin_response::PluginData(reader)) => {
|
||||
let reader = reader.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let name = reader
|
||||
.get_name()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let plugin_data_reader = reader
|
||||
.get_data()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
let plugin_data = plugin_data::deserialize_plugin_data(plugin_data_reader)?;
|
||||
|
||||
Ok(PluginResponse::PluginData(name.to_string(), plugin_data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::{EvaluatedCall, LabeledError, PluginCall, PluginResponse};
|
||||
use crate::protocol::{
|
||||
CallInput, EvaluatedCall, LabeledError, PluginCall, PluginData, PluginResponse,
|
||||
};
|
||||
use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value};
|
||||
|
||||
#[test]
|
||||
@ -216,6 +250,7 @@ mod tests {
|
||||
match returned {
|
||||
PluginCall::Signature => {}
|
||||
PluginCall::CallInfo(_) => panic!("decoded into wrong value"),
|
||||
PluginCall::CollapseCustomValue(_) => panic!("decoded into wrong value"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,10 +258,10 @@ mod tests {
|
||||
fn callinfo_round_trip_callinfo() {
|
||||
let name = "test".to_string();
|
||||
|
||||
let input = Value::Bool {
|
||||
let input = CallInput::Value(Value::Bool {
|
||||
val: false,
|
||||
span: Span { start: 1, end: 20 },
|
||||
};
|
||||
});
|
||||
|
||||
let call = EvaluatedCall {
|
||||
head: Span { start: 0, end: 10 },
|
||||
@ -252,11 +287,18 @@ mod tests {
|
||||
)],
|
||||
};
|
||||
|
||||
let plugin_call = PluginCall::CallInfo(Box::new(CallInfo {
|
||||
let plugin_call = PluginCall::CallInfo(CallInfo {
|
||||
name: name.clone(),
|
||||
call: call.clone(),
|
||||
input: input.clone(),
|
||||
}));
|
||||
// Avoiding having to implement Clone on CallInput just for tests
|
||||
input: match &input {
|
||||
CallInput::Value(value) => CallInput::Value(value.clone()),
|
||||
CallInput::Data(plugin_data) => CallInput::Data(PluginData {
|
||||
data: plugin_data.data.clone(),
|
||||
span: plugin_data.span,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
encode_call(&plugin_call, &mut buffer).expect("unable to serialize message");
|
||||
@ -266,7 +308,7 @@ mod tests {
|
||||
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!(&input, &call_info.input);
|
||||
assert_eq!(call.head, call_info.call.head);
|
||||
assert_eq!(call.positional.len(), call_info.call.positional.len());
|
||||
|
||||
@ -289,6 +331,31 @@ mod tests {
|
||||
}
|
||||
});
|
||||
}
|
||||
PluginCall::CollapseCustomValue(_) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callinfo_round_trip_collapsecustomvalue() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6, 7];
|
||||
let span = Span { start: 0, end: 20 };
|
||||
|
||||
let collapse_custom_value = PluginCall::CollapseCustomValue(PluginData {
|
||||
data: data.clone(),
|
||||
span,
|
||||
});
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
encode_call(&collapse_custom_value, &mut buffer).expect("unable to serialize message");
|
||||
let returned = decode_call(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||
|
||||
match returned {
|
||||
PluginCall::Signature => panic!("returned wrong call type"),
|
||||
PluginCall::CallInfo(_) => panic!("returned wrong call type"),
|
||||
PluginCall::CollapseCustomValue(plugin_data) => {
|
||||
assert_eq!(data, plugin_data.data);
|
||||
assert_eq!(span, plugin_data.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,6 +383,7 @@ mod tests {
|
||||
match returned {
|
||||
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
PluginResponse::Signature(returned_signature) => {
|
||||
assert!(returned_signature.len() == 1);
|
||||
assert_eq!(signature.name, returned_signature[0].name);
|
||||
@ -366,12 +434,45 @@ mod tests {
|
||||
match returned {
|
||||
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(returned_value) => {
|
||||
assert_eq!(&value, returned_value.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_plugin_data() {
|
||||
let name = "test".to_string();
|
||||
|
||||
let data = vec![1, 2, 3, 4, 5];
|
||||
let span = Span { start: 2, end: 30 };
|
||||
|
||||
let response = PluginResponse::PluginData(
|
||||
name.clone(),
|
||||
PluginData {
|
||||
data: data.clone(),
|
||||
span,
|
||||
},
|
||||
);
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
encode_response(&response, &mut buffer).expect("unable to serialize message");
|
||||
let returned =
|
||||
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(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(returned_name, returned_plugin_data) => {
|
||||
assert_eq!(name, returned_name);
|
||||
assert_eq!(data, returned_plugin_data.data);
|
||||
assert_eq!(span, returned_plugin_data.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_error() {
|
||||
let error = LabeledError {
|
||||
@ -390,6 +491,7 @@ mod tests {
|
||||
PluginResponse::Error(msg) => assert_eq!(error, msg),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,6 +513,7 @@ mod tests {
|
||||
PluginResponse::Error(msg) => assert_eq!(error, msg),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
79
crates/nu-plugin/src/serializers/capnp/plugin_data.rs
Normal file
79
crates/nu-plugin/src/serializers/capnp/plugin_data.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use crate::{plugin_capnp::plugin_data, protocol::PluginData};
|
||||
use nu_protocol::{ShellError, Span};
|
||||
|
||||
pub(crate) fn serialize_plugin_data(plugin_data: &PluginData, mut builder: plugin_data::Builder) {
|
||||
builder.set_data(&plugin_data.data);
|
||||
|
||||
let mut span_builder = builder.init_span();
|
||||
span_builder.set_start(plugin_data.span.start as u64);
|
||||
span_builder.set_end(plugin_data.span.end as u64);
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_plugin_data(
|
||||
reader: plugin_data::Reader,
|
||||
) -> Result<PluginData, ShellError> {
|
||||
let data = reader
|
||||
.get_data()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let span_reader = reader
|
||||
.get_span()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
let span = Span {
|
||||
start: span_reader.get_start() as usize,
|
||||
end: span_reader.get_end() as usize,
|
||||
};
|
||||
|
||||
Ok(PluginData {
|
||||
data: data.to_vec(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use capnp::serialize;
|
||||
use nu_protocol::Span;
|
||||
|
||||
pub fn write_buffer(
|
||||
plugin_data: &PluginData,
|
||||
writer: &mut impl std::io::Write,
|
||||
) -> Result<(), ShellError> {
|
||||
let mut message = ::capnp::message::Builder::new_default();
|
||||
|
||||
let mut builder = message.init_root::<plugin_data::Builder>();
|
||||
|
||||
serialize_plugin_data(plugin_data, builder.reborrow());
|
||||
|
||||
serialize::write_message(writer, &message)
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn read_buffer(reader: &mut impl std::io::BufRead) -> Result<PluginData, ShellError> {
|
||||
let message_reader =
|
||||
serialize::read_message(reader, ::capnp::message::ReaderOptions::new()).unwrap();
|
||||
|
||||
let reader = message_reader
|
||||
.get_root::<plugin_data::Reader>()
|
||||
.map_err(|e| ShellError::PluginFailedToDecode(e.to_string()))?;
|
||||
|
||||
deserialize_plugin_data(reader.reborrow())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_data_round_trip() {
|
||||
let plugin_data = PluginData {
|
||||
data: vec![1, 2, 3, 4, 5, 6, 7],
|
||||
span: Span { start: 1, end: 20 },
|
||||
};
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
write_buffer(&plugin_data, &mut buffer).expect("unable to serialize message");
|
||||
let returned_plugin_data =
|
||||
read_buffer(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||
|
||||
assert_eq!(plugin_data, returned_plugin_data)
|
||||
}
|
||||
}
|
@ -121,10 +121,22 @@ struct EvaluatedCall {
|
||||
named @2 :Map(Text, Value);
|
||||
}
|
||||
|
||||
struct PluginData {
|
||||
data @0 :Data;
|
||||
span @1 :Span;
|
||||
}
|
||||
|
||||
struct CallInput {
|
||||
union {
|
||||
value @0 :Value;
|
||||
pluginData @1 :PluginData;
|
||||
}
|
||||
}
|
||||
|
||||
struct CallInfo {
|
||||
name @0 :Text;
|
||||
call @1 :EvaluatedCall;
|
||||
input @2 :Value;
|
||||
input @2 :CallInput;
|
||||
}
|
||||
|
||||
# Main communication structs with the plugin
|
||||
@ -132,6 +144,7 @@ struct PluginCall {
|
||||
union {
|
||||
signature @0 :Void;
|
||||
callInfo @1 :CallInfo;
|
||||
collapseCustomValue @2 :PluginData;
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,6 +153,12 @@ struct PluginResponse {
|
||||
error @0 :LabeledError;
|
||||
signature @1 :List(Signature);
|
||||
value @2 :Value;
|
||||
pluginData @3 :PluginDataResponse;
|
||||
}
|
||||
|
||||
struct PluginDataResponse {
|
||||
name @0 :Text;
|
||||
data @1 :PluginData;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ use nu_protocol::ShellError;
|
||||
|
||||
use crate::{plugin::PluginEncoder, protocol::PluginResponse};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JsonSerializer;
|
||||
|
||||
impl PluginEncoder for JsonSerializer {
|
||||
@ -44,7 +44,9 @@ impl PluginEncoder for JsonSerializer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::{CallInfo, EvaluatedCall, LabeledError, PluginCall, PluginResponse};
|
||||
use crate::protocol::{
|
||||
CallInfo, CallInput, EvaluatedCall, LabeledError, PluginCall, PluginData, PluginResponse,
|
||||
};
|
||||
use nu_protocol::{Signature, Span, Spanned, SyntaxShape, Value};
|
||||
|
||||
#[test]
|
||||
@ -63,6 +65,7 @@ mod tests {
|
||||
match returned {
|
||||
PluginCall::Signature => {}
|
||||
PluginCall::CallInfo(_) => panic!("decoded into wrong value"),
|
||||
PluginCall::CollapseCustomValue(_) => panic!("decoded into wrong value"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,11 +102,11 @@ mod tests {
|
||||
)],
|
||||
};
|
||||
|
||||
let plugin_call = PluginCall::CallInfo(Box::new(CallInfo {
|
||||
let plugin_call = PluginCall::CallInfo(CallInfo {
|
||||
name: name.clone(),
|
||||
call: call.clone(),
|
||||
input: input.clone(),
|
||||
}));
|
||||
input: CallInput::Value(input.clone()),
|
||||
});
|
||||
|
||||
let encoder = JsonSerializer {};
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
@ -118,7 +121,7 @@ mod tests {
|
||||
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!(CallInput::Value(input), call_info.input);
|
||||
assert_eq!(call.head, call_info.call.head);
|
||||
assert_eq!(call.positional.len(), call_info.call.positional.len());
|
||||
|
||||
@ -141,6 +144,36 @@ mod tests {
|
||||
}
|
||||
});
|
||||
}
|
||||
PluginCall::CollapseCustomValue(_) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callinfo_round_trip_collapsecustomvalue() {
|
||||
let data = vec![1, 2, 3, 4, 5, 6, 7];
|
||||
let span = Span { start: 0, end: 20 };
|
||||
|
||||
let collapse_custom_value = PluginCall::CollapseCustomValue(PluginData {
|
||||
data: data.clone(),
|
||||
span,
|
||||
});
|
||||
|
||||
let encoder = JsonSerializer {};
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
encoder
|
||||
.encode_call(&collapse_custom_value, &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(_) => panic!("returned wrong call type"),
|
||||
PluginCall::CollapseCustomValue(plugin_data) => {
|
||||
assert_eq!(data, plugin_data.data);
|
||||
assert_eq!(span, plugin_data.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +205,7 @@ mod tests {
|
||||
match returned {
|
||||
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
PluginResponse::Signature(returned_signature) => {
|
||||
assert!(returned_signature.len() == 1);
|
||||
assert_eq!(signature.name, returned_signature[0].name);
|
||||
@ -226,12 +260,49 @@ mod tests {
|
||||
match returned {
|
||||
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(returned_value) => {
|
||||
assert_eq!(&value, returned_value.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_plugin_data() {
|
||||
let name = "test".to_string();
|
||||
|
||||
let data = vec![1, 2, 3, 4, 5];
|
||||
let span = Span { start: 2, end: 30 };
|
||||
|
||||
let response = PluginResponse::PluginData(
|
||||
name.clone(),
|
||||
PluginData {
|
||||
data: data.clone(),
|
||||
span,
|
||||
},
|
||||
);
|
||||
|
||||
let encoder = JsonSerializer {};
|
||||
let mut buffer: Vec<u8> = 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(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(returned_name, returned_plugin_data) => {
|
||||
assert_eq!(name, returned_name);
|
||||
assert_eq!(data, returned_plugin_data.data);
|
||||
assert_eq!(span, returned_plugin_data.span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_round_trip_error() {
|
||||
let error = LabeledError {
|
||||
@ -254,6 +325,7 @@ mod tests {
|
||||
PluginResponse::Error(msg) => assert_eq!(error, msg),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -279,6 +351,7 @@ mod tests {
|
||||
PluginResponse::Error(msg) => assert_eq!(error, msg),
|
||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||
PluginResponse::PluginData(..) => panic!("returned wrong call type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
pub mod capnp;
|
||||
pub mod json;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum EncodingType {
|
||||
Capnp(capnp::CapnpSerializer),
|
||||
Json(json::JsonSerializer),
|
||||
|
12
crates/nu_plugin_custom_values/Cargo.toml
Normal file
12
crates/nu_plugin_custom_values/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "nu_plugin_custom_values"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.65.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.65.1", features = ["plugin"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
typetag = "0.1.8"
|
67
crates/nu_plugin_custom_values/src/cool_custom_value.rs
Normal file
67
crates/nu_plugin_custom_values/src/cool_custom_value.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CoolCustomValue {
|
||||
pub(crate) cool: String,
|
||||
}
|
||||
|
||||
impl CoolCustomValue {
|
||||
pub fn new(content: &str) -> Self {
|
||||
Self {
|
||||
cool: content.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_value(value: &Value) -> Result<Self, ShellError> {
|
||||
match value {
|
||||
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
|
||||
Some(cool) => Ok(cool.clone()),
|
||||
None => Err(ShellError::CantConvert(
|
||||
"cool".into(),
|
||||
"non-cool".into(),
|
||||
*span,
|
||||
None,
|
||||
)),
|
||||
},
|
||||
x => Err(ShellError::CantConvert(
|
||||
"cool".into(),
|
||||
x.get_type().to_string(),
|
||||
x.span()?,
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for CoolCustomValue {
|
||||
fn clone_value(&self, span: nu_protocol::Span) -> Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self.clone()),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
self.typetag_name().to_string()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: nu_protocol::Span) -> Result<Value, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: format!("I used to be a custom value! My data was ({})", self.cool),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
78
crates/nu_plugin_custom_values/src/main.rs
Normal file
78
crates/nu_plugin_custom_values/src/main.rs
Normal file
@ -0,0 +1,78 @@
|
||||
mod cool_custom_value;
|
||||
mod second_custom_value;
|
||||
|
||||
use cool_custom_value::CoolCustomValue;
|
||||
use nu_plugin::{serve_plugin, CapnpSerializer, Plugin};
|
||||
use nu_plugin::{EvaluatedCall, LabeledError};
|
||||
use nu_protocol::{Category, ShellError, Signature, Value};
|
||||
use second_custom_value::SecondCustomValue;
|
||||
|
||||
struct CustomValuePlugin;
|
||||
|
||||
impl Plugin for CustomValuePlugin {
|
||||
fn signature(&self) -> Vec<nu_protocol::Signature> {
|
||||
vec![
|
||||
Signature::build("custom-value generate")
|
||||
.usage("Signature for a plugin that generates a custom value")
|
||||
.category(Category::Experimental),
|
||||
Signature::build("custom-value generate2")
|
||||
.usage("Signature for a plugin that generates a different custom value")
|
||||
.category(Category::Experimental),
|
||||
Signature::build("custom-value update")
|
||||
.usage("Signature for a plugin that updates a custom value")
|
||||
.category(Category::Experimental),
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
name: &str,
|
||||
call: &EvaluatedCall,
|
||||
input: &Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
match name {
|
||||
"custom-value generate" => self.generate(call, input),
|
||||
"custom-value generate2" => self.generate2(call, input),
|
||||
"custom-value update" => self.update(call, input),
|
||||
_ => Err(LabeledError {
|
||||
label: "Plugin call with wrong name 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),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomValuePlugin {
|
||||
fn generate(&mut self, call: &EvaluatedCall, _input: &Value) -> Result<Value, LabeledError> {
|
||||
Ok(CoolCustomValue::new("abc").into_value(call.head))
|
||||
}
|
||||
|
||||
fn generate2(&mut self, call: &EvaluatedCall, _input: &Value) -> Result<Value, LabeledError> {
|
||||
Ok(SecondCustomValue::new("xyz").into_value(call.head))
|
||||
}
|
||||
|
||||
fn update(&mut self, call: &EvaluatedCall, input: &Value) -> Result<Value, LabeledError> {
|
||||
if let Ok(mut value) = CoolCustomValue::try_from_value(input) {
|
||||
value.cool += "xyz";
|
||||
return Ok(value.into_value(call.head));
|
||||
}
|
||||
|
||||
if let Ok(mut value) = SecondCustomValue::try_from_value(input) {
|
||||
value.something += "abc";
|
||||
return Ok(value.into_value(call.head));
|
||||
}
|
||||
|
||||
Err(ShellError::CantConvert(
|
||||
"cool or second".into(),
|
||||
"non-cool and non-second".into(),
|
||||
call.head,
|
||||
None,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
serve_plugin(&mut CustomValuePlugin, CapnpSerializer {})
|
||||
}
|
70
crates/nu_plugin_custom_values/src/second_custom_value.rs
Normal file
70
crates/nu_plugin_custom_values/src/second_custom_value.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SecondCustomValue {
|
||||
pub(crate) something: String,
|
||||
}
|
||||
|
||||
impl SecondCustomValue {
|
||||
pub fn new(content: &str) -> Self {
|
||||
Self {
|
||||
something: content.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_value(value: &Value) -> Result<Self, ShellError> {
|
||||
match value {
|
||||
Value::CustomValue { val, span } => match val.as_any().downcast_ref::<Self>() {
|
||||
Some(value) => Ok(value.clone()),
|
||||
None => Err(ShellError::CantConvert(
|
||||
"cool".into(),
|
||||
"non-cool".into(),
|
||||
*span,
|
||||
None,
|
||||
)),
|
||||
},
|
||||
x => Err(ShellError::CantConvert(
|
||||
"cool".into(),
|
||||
x.get_type().to_string(),
|
||||
x.span()?,
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for SecondCustomValue {
|
||||
fn clone_value(&self, span: nu_protocol::Span) -> Value {
|
||||
Value::CustomValue {
|
||||
val: Box::new(self.clone()),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
fn value_string(&self) -> String {
|
||||
self.typetag_name().to_string()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: nu_protocol::Span) -> Result<Value, ShellError> {
|
||||
Ok(Value::String {
|
||||
val: format!(
|
||||
"I used to be a DIFFERENT custom value! ({})",
|
||||
self.something
|
||||
),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
84
tests/plugins/custom_values.rs
Normal file
84
tests/plugins/custom_values.rs
Normal file
@ -0,0 +1,84 @@
|
||||
use nu_test_support::nu_with_plugins;
|
||||
|
||||
#[test]
|
||||
fn can_get_custom_value_from_plugin_and_instantly_collapse_it() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests",
|
||||
plugin: ("capnp", "nu_plugin_custom_values"),
|
||||
"custom-value generate"
|
||||
);
|
||||
|
||||
assert_eq!(actual.out, "I used to be a custom value! My data was (abc)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_custom_value_from_plugin_and_pass_it_over() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests",
|
||||
plugin: ("capnp", "nu_plugin_custom_values"),
|
||||
"custom-value generate | custom-value update"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"I used to be a custom value! My data was (abcxyz)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_generate_and_updated_multiple_types_of_custom_values() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests",
|
||||
plugin: ("capnp", "nu_plugin_custom_values"),
|
||||
"custom-value generate2 | custom-value update"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"I used to be a DIFFERENT custom value! (xyzabc)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_describe_plugin_custom_values() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests",
|
||||
plugin: ("capnp", "nu_plugin_custom_values"),
|
||||
"custom-value generate | describe"
|
||||
);
|
||||
|
||||
assert_eq!(actual.out, "CoolCustomValue");
|
||||
}
|
||||
|
||||
// There are currently no custom values defined by the engine that aren't hidden behind an extra
|
||||
// feature, both database and dataframes are hidden behind --features=extra so we need to guard
|
||||
// this test
|
||||
#[cfg(feature = "database")]
|
||||
#[test]
|
||||
fn fails_if_passing_engine_custom_values_to_plugins() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests/fixtures/formats",
|
||||
plugin: ("capnp", "nu_plugin_custom_values"),
|
||||
"open-db sample.db | custom-value update"
|
||||
);
|
||||
|
||||
assert!(actual
|
||||
.err
|
||||
.contains("Plugin custom-value update can not handle the custom value SQLiteDatabase"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_if_passing_custom_values_across_plugins() {
|
||||
let actual = nu_with_plugins!(
|
||||
cwd: "tests",
|
||||
plugins: [
|
||||
("capnp", "nu_plugin_custom_values"),
|
||||
("json", "nu_plugin_inc")
|
||||
],
|
||||
"custom-value generate | inc --major"
|
||||
);
|
||||
|
||||
assert!(actual
|
||||
.err
|
||||
.contains("Plugin inc can not handle the custom value CoolCustomValue"));
|
||||
}
|
@ -1 +1,2 @@
|
||||
mod core_inc;
|
||||
mod custom_values;
|
||||
|
Loading…
Reference in New Issue
Block a user