Support for all custom value operations on plugin custom values (#12088)

# Description

Adds support for the following operations on plugin custom values, in
addition to `to_base_value` which was already present:

- `follow_path_int()`
- `follow_path_string()`
- `partial_cmp()`
- `operation()`
- `Drop` (notification, if opted into with
`CustomValue::notify_plugin_on_drop`)

There are additionally customizable methods within the `Plugin` and
`StreamingPlugin` traits for implementing these functions in a way that
requires access to the plugin state, as a registered handle model such
as might be used in a dataframes plugin would.

`Value::append` was also changed to handle custom values correctly.

# User-Facing Changes

- Signature of `CustomValue::follow_path_string` and
`CustomValue::follow_path_int` changed to give access to the span of the
custom value itself, useful for some errors.
- Plugins using custom values have to be recompiled because the engine
will try to do custom value operations that aren't supported
- Plugins can do more things 🎉 

# Tests + Formatting
Tests were added for all of the new custom values functionality.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] Document protocol reference `CustomValueOp` variants:
  - [ ] `FollowPathInt`
  - [ ] `FollowPathString`
  - [ ] `PartialCmp`
  - [ ] `Operation`
  - [ ] `Dropped`
- [ ] Document `notify_on_drop` optional field in `PluginCustomValue`
This commit is contained in:
Devyn Cairns
2024-03-12 02:37:08 -07:00
committed by GitHub
parent 8a250d2e08
commit 73f3c0b60b
19 changed files with 1065 additions and 156 deletions

View File

@ -1,7 +1,9 @@
use nu_protocol::{CustomValue, ShellError, Span, Value};
use std::cmp::Ordering;
use nu_protocol::{ast, CustomValue, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CoolCustomValue {
pub(crate) cool: String,
}
@ -44,7 +46,7 @@ impl CoolCustomValue {
#[typetag::serde]
impl CustomValue for CoolCustomValue {
fn clone_value(&self, span: nu_protocol::Span) -> Value {
fn clone_value(&self, span: Span) -> Value {
Value::custom_value(Box::new(self.clone()), span)
}
@ -52,13 +54,94 @@ impl CustomValue for CoolCustomValue {
self.typetag_name().to_string()
}
fn to_base_value(&self, span: nu_protocol::Span) -> Result<Value, ShellError> {
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
Ok(Value::string(
format!("I used to be a custom value! My data was ({})", self.cool),
span,
))
}
fn follow_path_int(
&self,
_self_span: Span,
index: usize,
path_span: Span,
) -> Result<Value, ShellError> {
if index == 0 {
Ok(Value::string(&self.cool, path_span))
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: 0,
span: path_span,
})
}
}
fn follow_path_string(
&self,
self_span: Span,
column_name: String,
path_span: Span,
) -> Result<Value, ShellError> {
if column_name == "cool" {
Ok(Value::string(&self.cool, path_span))
} else {
Err(ShellError::CantFindColumn {
col_name: column_name,
span: path_span,
src_span: self_span,
})
}
}
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
if let Value::CustomValue { val, .. } = other {
val.as_any()
.downcast_ref()
.and_then(|other: &CoolCustomValue| PartialOrd::partial_cmp(self, other))
} else {
None
}
}
fn operation(
&self,
lhs_span: Span,
operator: ast::Operator,
op_span: Span,
right: &Value,
) -> Result<Value, ShellError> {
match operator {
// Append the string inside `cool`
ast::Operator::Math(ast::Math::Append) => {
if let Some(right) = right
.as_custom_value()
.ok()
.and_then(|c| c.as_any().downcast_ref::<CoolCustomValue>())
{
Ok(Value::custom_value(
Box::new(CoolCustomValue {
cool: format!("{}{}", self.cool, right.cool),
}),
op_span,
))
} else {
Err(ShellError::OperatorMismatch {
op_span,
lhs_ty: self.typetag_name().into(),
lhs_span,
rhs_ty: right.get_type().to_string(),
rhs_span: right.span(),
})
}
}
_ => Err(ShellError::UnsupportedOperator {
operator,
span: op_span,
}),
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}

View File

@ -0,0 +1,50 @@
use nu_protocol::{record, CustomValue, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DropCheck {
pub(crate) msg: String,
}
impl DropCheck {
pub(crate) fn new(msg: String) -> DropCheck {
DropCheck { msg }
}
pub(crate) fn into_value(self, span: Span) -> Value {
Value::custom_value(Box::new(self), span)
}
pub(crate) fn notify(&self) {
eprintln!("DropCheck was dropped: {}", self.msg);
}
}
#[typetag::serde]
impl CustomValue for DropCheck {
fn clone_value(&self, span: Span) -> Value {
self.clone().into_value(span)
}
fn value_string(&self) -> String {
"DropCheck".into()
}
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
Ok(Value::record(
record! {
"msg" => Value::string(&self.msg, span)
},
span,
))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn notify_plugin_on_drop(&self) -> bool {
// This is what causes Nushell to let us know when the value is dropped
true
}
}

View File

@ -1,11 +1,14 @@
mod cool_custom_value;
mod drop_check;
mod second_custom_value;
use cool_custom_value::CoolCustomValue;
use drop_check::DropCheck;
use second_custom_value::SecondCustomValue;
use nu_plugin::{serve_plugin, EngineInterface, MsgPackSerializer, Plugin};
use nu_plugin::{EvaluatedCall, LabeledError};
use nu_protocol::{Category, PluginSignature, ShellError, SyntaxShape, Value};
use second_custom_value::SecondCustomValue;
use nu_protocol::{Category, CustomValue, PluginSignature, ShellError, SyntaxShape, Value};
struct CustomValuePlugin;
@ -34,6 +37,10 @@ impl Plugin for CustomValuePlugin {
"the custom value to update",
)
.category(Category::Experimental),
PluginSignature::build("custom-value drop-check")
.usage("Generates a custom value that prints a message when dropped")
.required("msg", SyntaxShape::String, "the message to print on drop")
.category(Category::Experimental),
]
}
@ -49,6 +56,7 @@ impl Plugin for CustomValuePlugin {
"custom-value generate2" => self.generate2(engine, call),
"custom-value update" => self.update(call, input),
"custom-value update-arg" => self.update(call, &call.req(0)?),
"custom-value drop-check" => self.drop_check(call),
_ => 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(),
@ -56,6 +64,18 @@ impl Plugin for CustomValuePlugin {
}),
}
}
fn custom_value_dropped(
&self,
_engine: &EngineInterface,
custom_value: Box<dyn CustomValue>,
) -> Result<(), LabeledError> {
// This is how we implement our drop behavior for DropCheck.
if let Some(drop_check) = custom_value.as_any().downcast_ref::<DropCheck>() {
drop_check.notify();
}
Ok(())
}
}
impl CustomValuePlugin {
@ -101,6 +121,10 @@ impl CustomValuePlugin {
}
.into())
}
fn drop_check(&self, call: &EvaluatedCall) -> Result<Value, LabeledError> {
Ok(DropCheck::new(call.req(0)?).into_value(call.head))
}
}
fn main() {