Skip to content

Commit 20b0faa

Browse files
authored
Allow saving CustomValues (nushell#16692)
1 parent 2c8bcc8 commit 20b0faa

File tree

10 files changed

+206
-41
lines changed

10 files changed

+206
-41
lines changed

crates/nu-command/src/filesystem/save.rs

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use nu_protocol::{
88
byte_stream::copy_with_signals, process::ChildPipe, shell_error::io::IoError,
99
};
1010
use std::{
11+
borrow::Cow,
1112
fs::File,
1213
io::{self, BufRead, BufReader, Read, Write},
1314
path::{Path, PathBuf},
@@ -224,8 +225,28 @@ impl Command for Save {
224225
)?;
225226
}
226227

227-
let bytes =
228-
input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?;
228+
// Try to convert the input pipeline into another type if we know the extension
229+
let ext = extract_extension(&input, &path.item, raw);
230+
let converted = match ext {
231+
None => input,
232+
Some(ext) => convert_to_extension(engine_state, &ext, stack, input, span)?,
233+
};
234+
235+
// Save custom value however they implement saving
236+
if let PipelineData::Value(Value::Custom { val, internal_span }, ..) = converted {
237+
return val
238+
.save(
239+
Spanned {
240+
item: &path.item,
241+
span: path.span,
242+
},
243+
internal_span,
244+
span,
245+
)
246+
.map(|()| PipelineData::empty());
247+
}
248+
249+
let bytes = value_to_bytes(converted.into_value(span)?)?;
229250

230251
// Only open file after successful conversion
231252
let (mut file, _) =
@@ -321,34 +342,14 @@ fn check_saving_to_source_file(
321342
Ok(())
322343
}
323344

324-
/// Convert [`PipelineData`] bytes to write in file, possibly converting
325-
/// to format of output file
326-
fn input_to_bytes(
327-
input: PipelineData,
328-
path: &Path,
329-
raw: bool,
330-
engine_state: &EngineState,
331-
stack: &mut Stack,
332-
span: Span,
333-
) -> Result<Vec<u8>, ShellError> {
334-
let ext = if raw {
335-
None
336-
} else if let PipelineData::ByteStream(..) = input {
337-
None
338-
} else if let PipelineData::Value(Value::String { .. }, ..) = input {
339-
None
340-
} else {
341-
path.extension()
342-
.map(|name| name.to_string_lossy().to_string())
343-
};
344-
345-
let input = if let Some(ext) = ext {
346-
convert_to_extension(engine_state, &ext, stack, input, span)?
347-
} else {
348-
input
349-
};
350-
351-
value_to_bytes(input.into_value(span)?)
345+
/// Extract extension for conversion.
346+
fn extract_extension<'e>(input: &PipelineData, path: &'e Path, raw: bool) -> Option<Cow<'e, str>> {
347+
match (raw, input) {
348+
(true, _)
349+
| (_, PipelineData::ByteStream(..))
350+
| (_, PipelineData::Value(Value::String { .. }, ..)) => None,
351+
_ => path.extension().map(|name| name.to_string_lossy()),
352+
}
352353
}
353354

354355
/// Convert given data into content of file of specified extension if

crates/nu-plugin-engine/src/interface/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use nu_protocol::{
1717
use nu_utils::SharedCow;
1818
use std::{
1919
collections::{BTreeMap, btree_map},
20+
path::Path,
2021
sync::{Arc, OnceLock, mpsc},
2122
};
2223

@@ -1059,6 +1060,32 @@ impl PluginInterface {
10591060
self.custom_value_op_expecting_value(left, CustomValueOp::Operation(operator, right))
10601061
}
10611062

1063+
/// Invoke saving operation on a custom value.
1064+
pub fn custom_value_save(
1065+
&self,
1066+
value: Spanned<PluginCustomValueWithSource>,
1067+
path: Spanned<&Path>,
1068+
save_call_span: Span,
1069+
) -> Result<(), ShellError> {
1070+
// Check that the value came from the right source
1071+
value.item.verify_source(value.span, &self.state.source)?;
1072+
1073+
let call = PluginCall::CustomValueOp(
1074+
value.map(|cv| cv.without_source()),
1075+
CustomValueOp::Save {
1076+
path: path.map(ToOwned::to_owned),
1077+
save_call_span,
1078+
},
1079+
);
1080+
match self.plugin_call(call, None)? {
1081+
PluginCallResponse::Ok => Ok(()),
1082+
PluginCallResponse::Error(err) => Err(err.into()),
1083+
_ => Err(ShellError::PluginFailedToDecode {
1084+
msg: "Received unexpected response to custom value save() call".into(),
1085+
}),
1086+
}
1087+
}
1088+
10621089
/// Notify the plugin about a dropped custom value.
10631090
pub fn custom_value_dropped(&self, value: PluginCustomValue) -> Result<(), ShellError> {
10641091
// Make sure we don't block here. This can happen on the receiver thread, which would cause a deadlock. We should not try to receive the response - just let it be discarded.
@@ -1253,6 +1280,7 @@ impl CurrentCallState {
12531280
CustomValueOp::FollowPathString { .. } => Ok(()),
12541281
CustomValueOp::PartialCmp(value) => self.prepare_value(value, source),
12551282
CustomValueOp::Operation(_, value) => self.prepare_value(value, source),
1283+
CustomValueOp::Save { .. } => Ok(()),
12561284
CustomValueOp::Dropped => Ok(()),
12571285
}
12581286
}

crates/nu-plugin-engine/src/plugin_custom_value_with_source/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cmp::Ordering, sync::Arc};
1+
use std::{cmp::Ordering, path::Path, sync::Arc};
22

33
use nu_plugin_core::util::with_custom_values_in;
44
use nu_plugin_protocol::PluginCustomValue;
@@ -235,6 +235,16 @@ impl CustomValue for PluginCustomValueWithSource {
235235
)
236236
}
237237

238+
fn save(
239+
&self,
240+
path: Spanned<&Path>,
241+
value_span: Span,
242+
save_span: Span,
243+
) -> Result<(), ShellError> {
244+
self.get_plugin(Some(value_span), "save")?
245+
.custom_value_save(self.clone().into_spanned(value_span), path, save_span)
246+
}
247+
238248
fn as_any(&self) -> &dyn std::any::Any {
239249
self
240250
}

crates/nu-plugin-protocol/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use nu_protocol::{
2828
};
2929
use nu_utils::SharedCow;
3030
use serde::{Deserialize, Serialize};
31-
use std::collections::HashMap;
31+
use std::{collections::HashMap, path::PathBuf};
3232

3333
pub use evaluated_call::EvaluatedCall;
3434
pub use plugin_custom_value::PluginCustomValue;
@@ -212,6 +212,11 @@ pub enum CustomValueOp {
212212
PartialCmp(Value),
213213
/// [`operation()`](nu_protocol::CustomValue::operation)
214214
Operation(Spanned<Operator>, Value),
215+
/// [`save()`](nu_protocol::CustomValue::save)
216+
Save {
217+
path: Spanned<PathBuf>,
218+
save_call_span: Span,
219+
},
215220
/// Notify that the custom value has been dropped, if
216221
/// [`notify_plugin_on_drop()`](nu_protocol::CustomValue::notify_plugin_on_drop) is true
217222
Dropped,
@@ -226,6 +231,7 @@ impl CustomValueOp {
226231
CustomValueOp::FollowPathString { .. } => "follow_path_string",
227232
CustomValueOp::PartialCmp(_) => "partial_cmp",
228233
CustomValueOp::Operation(_, _) => "operation",
234+
CustomValueOp::Save { .. } => "save",
229235
CustomValueOp::Dropped => "dropped",
230236
}
231237
}
@@ -359,6 +365,7 @@ pub enum StreamMessage {
359365
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
360366
#[derive(Serialize, Deserialize, Debug, Clone)]
361367
pub enum PluginCallResponse<D> {
368+
Ok,
362369
Error(LabeledError),
363370
Metadata(PluginMetadata),
364371
Signature(Vec<PluginSignature>),
@@ -374,6 +381,7 @@ impl<D> PluginCallResponse<D> {
374381
f: impl FnOnce(D) -> Result<T, ShellError>,
375382
) -> Result<PluginCallResponse<T>, ShellError> {
376383
Ok(match self {
384+
PluginCallResponse::Ok => PluginCallResponse::Ok,
377385
PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
378386
PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta),
379387
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),

crates/nu-plugin-protocol/src/plugin_custom_value/mod.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::cmp::Ordering;
1+
use std::{cmp::Ordering, path::Path};
22

3-
use nu_protocol::{CustomValue, ShellError, Span, Value, ast::Operator, casing::Casing};
3+
use nu_protocol::{CustomValue, ShellError, Span, Spanned, Value, ast::Operator, casing::Casing};
44
use nu_utils::SharedCow;
55

66
use serde::{Deserialize, Serialize};
@@ -93,6 +93,15 @@ impl CustomValue for PluginCustomValue {
9393
panic!("operation() not available on plugin custom value without source");
9494
}
9595

96+
fn save(
97+
&self,
98+
_path: Spanned<&Path>,
99+
_value_span: Span,
100+
_save_span: Span,
101+
) -> Result<(), ShellError> {
102+
panic!("save() not available on plugin custom value without source");
103+
}
104+
96105
fn as_any(&self) -> &dyn std::any::Any {
97106
self
98107
}

crates/nu-plugin/src/plugin/interface/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ impl EngineInterface {
411411
})
412412
}
413413

414+
/// Write an OK call response or an error.
415+
pub(crate) fn write_ok(
416+
&self,
417+
result: Result<(), impl Into<LabeledError>>,
418+
) -> Result<(), ShellError> {
419+
let response = match result {
420+
Ok(()) => PluginCallResponse::Ok,
421+
Err(err) => PluginCallResponse::Error(err.into()),
422+
};
423+
self.write(PluginOutput::CallResponse(self.context()?, response))?;
424+
self.flush()
425+
}
426+
414427
/// Write a call response of either [`PipelineData`] or an error. Returns the stream writer
415428
/// to finish writing the stream
416429
pub(crate) fn write_response(

crates/nu-plugin/src/plugin/mod.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{
55
ffi::OsString,
66
ops::Deref,
77
panic::AssertUnwindSafe,
8+
path::Path,
89
sync::mpsc::{self, TrySendError},
910
thread,
1011
};
@@ -16,8 +17,8 @@ use nu_plugin_core::{
1617
};
1718
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
1819
use nu_protocol::{
19-
CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata, ShellError, Spanned,
20-
Value, ast::Operator, casing::Casing,
20+
CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata, ShellError, Span,
21+
Spanned, Value, ast::Operator, casing::Casing,
2122
};
2223
use thiserror::Error;
2324

@@ -214,6 +215,24 @@ pub trait Plugin: Sync {
214215
.map_err(LabeledError::from)
215216
}
216217

218+
/// Implement saving logic for a custom value.
219+
///
220+
/// The default implementation of this method just calls [`CustomValue::save`], but
221+
/// the method can be implemented differently if accessing plugin state is desirable.
222+
fn custom_value_save(
223+
&self,
224+
engine: &EngineInterface,
225+
value: Spanned<Box<dyn CustomValue>>,
226+
path: Spanned<&Path>,
227+
save_call_span: Span,
228+
) -> Result<(), LabeledError> {
229+
let _ = engine;
230+
value
231+
.item
232+
.save(path, value.span, save_call_span)
233+
.map_err(LabeledError::from)
234+
}
235+
217236
/// Handle a notification that all copies of a custom value within the engine have been dropped.
218237
///
219238
/// This notification is only sent if [`CustomValue::notify_plugin_on_drop`] was true. Unlike
@@ -652,6 +671,17 @@ fn custom_value_op(
652671
.write_response(result)
653672
.and_then(|writer| writer.write())
654673
}
674+
CustomValueOp::Save {
675+
path,
676+
save_call_span,
677+
} => {
678+
let path = Spanned {
679+
item: path.item.as_path(),
680+
span: path.span,
681+
};
682+
let result = plugin.custom_value_save(engine, local_value, path, save_call_span);
683+
engine.write_ok(result)
684+
}
655685
CustomValueOp::Dropped => {
656686
let result = plugin
657687
.custom_value_dropped(engine, local_value.item)

crates/nu-protocol/src/value/custom_value.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use std::{cmp::Ordering, fmt};
1+
use std::{cmp::Ordering, fmt, path::Path};
22

3-
use crate::{ShellError, Span, Type, Value, ast::Operator, casing::Casing};
3+
use crate::{ShellError, Span, Spanned, Type, Value, ast::Operator, casing::Casing};
44

55
/// Trait definition for a custom [`Value`](crate::Value) type
66
#[typetag::serde(tag = "type")]
@@ -110,6 +110,35 @@ pub trait CustomValue: fmt::Debug + Send + Sync {
110110
})
111111
}
112112

113+
/// Save custom value to disk.
114+
///
115+
/// This method is used in `save` to save a custom value to disk.
116+
/// This is done before opening any file, so saving can be handled differently.
117+
///
118+
/// The default impl just returns an error.
119+
fn save(
120+
&self,
121+
path: Spanned<&Path>,
122+
value_span: Span,
123+
save_span: Span,
124+
) -> Result<(), ShellError> {
125+
let _ = path;
126+
Err(ShellError::GenericError {
127+
error: "Cannot save custom value".into(),
128+
msg: format!("Saving custom value {} failed", self.type_name()),
129+
span: Some(save_span),
130+
help: None,
131+
inner: vec![
132+
ShellError::GenericError {
133+
error: "Custom value does not implement `save`".into(),
134+
msg: format!("{} doesn't implement saving to disk", self.type_name()),
135+
span: Some(value_span),
136+
help: Some("Check the plugin's documentation for this value type. It might use a different way to save.".into()),
137+
inner: vec![],
138+
}],
139+
})
140+
}
141+
113142
/// For custom values in plugins: return `true` here if you would like to be notified when all
114143
/// copies of this custom value are dropped in the engine.
115144
///

crates/nu_plugin_custom_values/src/second_custom_value.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![allow(clippy::result_large_err)]
2-
use nu_protocol::{CustomValue, ShellError, Span, Value};
2+
use nu_protocol::{CustomValue, ShellError, Span, Spanned, Value, shell_error::io::IoError};
33
use serde::{Deserialize, Serialize};
4-
use std::cmp::Ordering;
4+
use std::{cmp::Ordering, path::Path};
55

66
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
77
pub struct SecondCustomValue {
@@ -78,4 +78,15 @@ impl CustomValue for SecondCustomValue {
7878
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
7979
self
8080
}
81+
82+
fn save(&self, path: Spanned<&Path>, _: Span, save_span: Span) -> Result<(), ShellError> {
83+
std::fs::write(path.item, &self.something).map_err(|err| {
84+
ShellError::Io(IoError::new_with_additional_context(
85+
err,
86+
save_span,
87+
path.item.to_owned(),
88+
format!("Could not save {}", self.type_name()),
89+
))
90+
})
91+
}
8192
}

0 commit comments

Comments
 (0)