more closure serialization (#14698)

# Description

This PR introduces a switch `--serialize` that allows serializing of
types that cannot be deserialized. Right now it only serializes closures
as strings in `to toml`, `to json`, `to nuon`, `to text`, some indirect
`to html` and `to yaml`.

A lot of the changes are just weaving the engine_state through calling
functions and the rest is just repetitive way of getting the closure
block span and grabbing the span's text.

In places where it has to report `<Closure 123>` I changed it to
`closure_123`. It always seemed like the `<>` were not very nushell-y.
This is still a breaking change.

I think this could also help with systematic translation of old config
to new config file.


# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
Darren Schroeder 2025-01-07 11:51:22 -06:00 committed by GitHub
parent 1f477c8eb1
commit dad956b2ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 509 additions and 113 deletions

View File

@ -154,7 +154,8 @@ fn get_arguments(
eval_expression_fn, eval_expression_fn,
); );
let arg_type = "expr"; let arg_type = "expr";
let arg_value_name = debug_string_without_formatting(&evaluated_expression); let arg_value_name =
debug_string_without_formatting(engine_state, &evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string(); let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.span(); let evaled_span = evaluated_expression.span();
let arg_value_name_span_start = evaled_span.start as i64; let arg_value_name_span_start = evaled_span.start as i64;
@ -174,7 +175,8 @@ fn get_arguments(
let arg_type = "positional"; let arg_type = "positional";
let evaluated_expression = let evaluated_expression =
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn); get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
let arg_value_name = debug_string_without_formatting(&evaluated_expression); let arg_value_name =
debug_string_without_formatting(engine_state, &evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string(); let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.span(); let evaled_span = evaluated_expression.span();
let arg_value_name_span_start = evaled_span.start as i64; let arg_value_name_span_start = evaled_span.start as i64;
@ -193,7 +195,8 @@ fn get_arguments(
let arg_type = "unknown"; let arg_type = "unknown";
let evaluated_expression = let evaluated_expression =
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn); get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
let arg_value_name = debug_string_without_formatting(&evaluated_expression); let arg_value_name =
debug_string_without_formatting(engine_state, &evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string(); let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.span(); let evaled_span = evaluated_expression.span();
let arg_value_name_span_start = evaled_span.start as i64; let arg_value_name_span_start = evaled_span.start as i64;
@ -212,7 +215,8 @@ fn get_arguments(
let arg_type = "spread"; let arg_type = "spread";
let evaluated_expression = let evaluated_expression =
get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn); get_expression_as_value(engine_state, stack, inner_expr, eval_expression_fn);
let arg_value_name = debug_string_without_formatting(&evaluated_expression); let arg_value_name =
debug_string_without_formatting(engine_state, &evaluated_expression);
let arg_value_type = &evaluated_expression.get_type().to_string(); let arg_value_type = &evaluated_expression.get_type().to_string();
let evaled_span = evaluated_expression.span(); let evaled_span = evaluated_expression.span();
let arg_value_name_span_start = evaled_span.start as i64; let arg_value_name_span_start = evaled_span.start as i64;
@ -245,7 +249,7 @@ fn get_expression_as_value(
} }
} }
pub fn debug_string_without_formatting(value: &Value) -> String { pub fn debug_string_without_formatting(engine_state: &EngineState, value: &Value) -> String {
match value { match value {
Value::Bool { val, .. } => val.to_string(), Value::Bool { val, .. } => val.to_string(),
Value::Int { val, .. } => val.to_string(), Value::Int { val, .. } => val.to_string(),
@ -259,19 +263,31 @@ pub fn debug_string_without_formatting(value: &Value) -> String {
Value::List { vals: val, .. } => format!( Value::List { vals: val, .. } => format!(
"[{}]", "[{}]",
val.iter() val.iter()
.map(debug_string_without_formatting) .map(|v| debug_string_without_formatting(engine_state, v))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
), ),
Value::Record { val, .. } => format!( Value::Record { val, .. } => format!(
"{{{}}}", "{{{}}}",
val.iter() val.iter()
.map(|(x, y)| format!("{}: {}", x, debug_string_without_formatting(y))) .map(|(x, y)| format!(
"{}: {}",
x,
debug_string_without_formatting(engine_state, y)
))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
), ),
//TODO: It would be good to drill deeper into closures. Value::Closure { val, .. } => {
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id.get()), let block = engine_state.get_block(val.block_id);
if let Some(span) = block.span {
let contents_bytes = engine_state.get_span_contents(span);
let contents_string = String::from_utf8_lossy(contents_bytes);
contents_string.to_string()
} else {
String::new()
}
}
Value::Nothing { .. } => String::new(), Value::Nothing { .. } => String::new(),
Value::Error { error, .. } => format!("{error:?}"), Value::Error { error, .. } => format!("{error:?}"),
Value::Binary { val, .. } => format!("{val:?}"), Value::Binary { val, .. } => format!("{val:?}"),
@ -280,7 +296,7 @@ pub fn debug_string_without_formatting(value: &Value) -> String {
// that critical here // that critical here
Value::Custom { val, .. } => val Value::Custom { val, .. } => val
.to_base_value(value.span()) .to_base_value(value.span())
.map(|val| debug_string_without_formatting(&val)) .map(|val| debug_string_without_formatting(engine_state, &val))
.unwrap_or_else(|_| format!("<{}>", val.type_name())), .unwrap_or_else(|_| format!("<{}>", val.type_name())),
} }
} }

View File

@ -23,7 +23,7 @@ impl Command for Inspect {
fn run( fn run(
&self, &self,
_engine_state: &EngineState, engine_state: &EngineState,
_stack: &mut Stack, _stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
@ -40,7 +40,7 @@ impl Command for Inspect {
let (cols, _rows) = terminal_size().unwrap_or((0, 0)); let (cols, _rows) = terminal_size().unwrap_or((0, 0));
let table = inspect_table::build_table(input_val, description, cols as usize); let table = inspect_table::build_table(engine_state, input_val, description, cols as usize);
// Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell // Note that this is printed to stderr. The reason for this is so it doesn't disrupt the regular nushell
// tabular output. If we printed to stdout, nushell would get confused with two outputs. // tabular output. If we printed to stdout, nushell would get confused with two outputs.

View File

@ -1,19 +1,23 @@
// note: Seems like could be simplified // note: Seems like could be simplified
// IMHO: it shall not take 300+ lines :) // IMHO: it shall not take 300+ lines :)
use self::{global_horizontal_char::SetHorizontalChar, set_widths::SetWidths};
use nu_protocol::engine::EngineState;
use nu_protocol::Value; use nu_protocol::Value;
use nu_table::{string_width, string_wrap}; use nu_table::{string_width, string_wrap};
use tabled::{ use tabled::{
grid::config::ColoredConfig, grid::config::ColoredConfig,
settings::{peaker::Priority, width::Wrap, Settings, Style}, settings::{peaker::Priority, width::Wrap, Settings, Style},
Table, Table,
}; };
use self::{global_horizontal_char::SetHorizontalChar, set_widths::SetWidths}; pub fn build_table(
engine_state: &EngineState,
pub fn build_table(value: Value, description: String, termsize: usize) -> String { value: Value,
let (head, mut data) = util::collect_input(value); description: String,
termsize: usize,
) -> String {
let (head, mut data) = util::collect_input(engine_state, value);
let count_columns = head.len(); let count_columns = head.len();
data.insert(0, head); data.insert(0, head);
@ -195,10 +199,14 @@ fn push_empty_column(data: &mut Vec<Vec<String>>) {
mod util { mod util {
use crate::debug::explain::debug_string_without_formatting; use crate::debug::explain::debug_string_without_formatting;
use nu_engine::get_columns; use nu_engine::get_columns;
use nu_protocol::engine::EngineState;
use nu_protocol::Value; use nu_protocol::Value;
/// Try to build column names and a table grid. /// Try to build column names and a table grid.
pub fn collect_input(value: Value) -> (Vec<String>, Vec<Vec<String>>) { pub fn collect_input(
engine_state: &EngineState,
value: Value,
) -> (Vec<String>, Vec<Vec<String>>) {
let span = value.span(); let span = value.span();
match value { match value {
Value::Record { val: record, .. } => { Value::Record { val: record, .. } => {
@ -210,7 +218,7 @@ mod util {
}, },
match vals match vals
.into_iter() .into_iter()
.map(|s| debug_string_without_formatting(&s)) .map(|s| debug_string_without_formatting(engine_state, &s))
.collect::<Vec<String>>() .collect::<Vec<String>>()
{ {
vals if vals.is_empty() => vec![], vals if vals.is_empty() => vec![],
@ -220,7 +228,7 @@ mod util {
} }
Value::List { vals, .. } => { Value::List { vals, .. } => {
let mut columns = get_columns(&vals); let mut columns = get_columns(&vals);
let data = convert_records_to_dataset(&columns, vals); let data = convert_records_to_dataset(engine_state, &columns, vals);
if columns.is_empty() { if columns.is_empty() {
columns = vec![String::from("")]; columns = vec![String::from("")];
@ -232,7 +240,7 @@ mod util {
let lines = val let lines = val
.lines() .lines()
.map(|line| Value::string(line.to_string(), span)) .map(|line| Value::string(line.to_string(), span))
.map(|val| vec![debug_string_without_formatting(&val)]) .map(|val| vec![debug_string_without_formatting(engine_state, &val)])
.collect(); .collect();
(vec![String::from("")], lines) (vec![String::from("")], lines)
@ -240,47 +248,59 @@ mod util {
Value::Nothing { .. } => (vec![], vec![]), Value::Nothing { .. } => (vec![], vec![]),
value => ( value => (
vec![String::from("")], vec![String::from("")],
vec![vec![debug_string_without_formatting(&value)]], vec![vec![debug_string_without_formatting(engine_state, &value)]],
), ),
} }
} }
fn convert_records_to_dataset(cols: &[String], records: Vec<Value>) -> Vec<Vec<String>> { fn convert_records_to_dataset(
engine_state: &EngineState,
cols: &[String],
records: Vec<Value>,
) -> Vec<Vec<String>> {
if !cols.is_empty() { if !cols.is_empty() {
create_table_for_record(cols, &records) create_table_for_record(engine_state, cols, &records)
} else if cols.is_empty() && records.is_empty() { } else if cols.is_empty() && records.is_empty() {
vec![] vec![]
} else if cols.len() == records.len() { } else if cols.len() == records.len() {
vec![records vec![records
.into_iter() .into_iter()
.map(|s| debug_string_without_formatting(&s)) .map(|s| debug_string_without_formatting(engine_state, &s))
.collect()] .collect()]
} else { } else {
records records
.into_iter() .into_iter()
.map(|record| vec![debug_string_without_formatting(&record)]) .map(|record| vec![debug_string_without_formatting(engine_state, &record)])
.collect() .collect()
} }
} }
fn create_table_for_record(headers: &[String], items: &[Value]) -> Vec<Vec<String>> { fn create_table_for_record(
engine_state: &EngineState,
headers: &[String],
items: &[Value],
) -> Vec<Vec<String>> {
let mut data = vec![Vec::new(); items.len()]; let mut data = vec![Vec::new(); items.len()];
for (i, item) in items.iter().enumerate() { for (i, item) in items.iter().enumerate() {
let row = record_create_row(headers, item); let row = record_create_row(engine_state, headers, item);
data[i] = row; data[i] = row;
} }
data data
} }
fn record_create_row(headers: &[String], item: &Value) -> Vec<String> { fn record_create_row(
engine_state: &EngineState,
headers: &[String],
item: &Value,
) -> Vec<String> {
if let Value::Record { val, .. } = item { if let Value::Record { val, .. } = item {
headers headers
.iter() .iter()
.map(|col| { .map(|col| {
val.get(col) val.get(col)
.map(debug_string_without_formatting) .map(|v| debug_string_without_formatting(engine_state, v))
.unwrap_or_else(String::new) .unwrap_or_else(String::new)
}) })
.collect() .collect()

View File

@ -213,9 +213,15 @@ fn sort_attributes(val: Value) -> Value {
} }
} }
fn generate_key(item: &ValueCounter) -> Result<String, ShellError> { fn generate_key(engine_state: &EngineState, item: &ValueCounter) -> Result<String, ShellError> {
let value = sort_attributes(item.val_to_compare.clone()); //otherwise, keys could be different for Records let value = sort_attributes(item.val_to_compare.clone()); //otherwise, keys could be different for Records
nuon::to_nuon(&value, nuon::ToStyle::Raw, Some(Span::unknown())) nuon::to_nuon(
engine_state,
&value,
nuon::ToStyle::Raw,
Some(Span::unknown()),
false,
)
} }
fn generate_results_with_count(head: Span, uniq_values: Vec<ValueCounter>) -> Vec<Value> { fn generate_results_with_count(head: Span, uniq_values: Vec<ValueCounter>) -> Vec<Value> {
@ -264,7 +270,7 @@ pub fn uniq(
.try_fold( .try_fold(
HashMap::<String, ValueCounter>::new(), HashMap::<String, ValueCounter>::new(),
|mut counter, item| { |mut counter, item| {
let key = generate_key(&item); let key = generate_key(engine_state, &item);
match key { match key {
Ok(key) => { Ok(key) => {

View File

@ -25,6 +25,11 @@ impl Command for ToJson {
"specify indentation tab quantity", "specify indentation tab quantity",
Some('t'), Some('t'),
) )
.switch(
"serialize",
"serialize nushell types that cannot be deserialized",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -42,12 +47,13 @@ impl Command for ToJson {
let raw = call.has_flag(engine_state, stack, "raw")?; let raw = call.has_flag(engine_state, stack, "raw")?;
let use_tabs = call.get_flag(engine_state, stack, "tabs")?; let use_tabs = call.get_flag(engine_state, stack, "tabs")?;
let indent = call.get_flag(engine_state, stack, "indent")?; let indent = call.get_flag(engine_state, stack, "indent")?;
let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
let span = call.head; let span = call.head;
// allow ranges to expand and turn into array // allow ranges to expand and turn into array
let input = input.try_expand_range()?; let input = input.try_expand_range()?;
let value = input.into_value(span)?; let value = input.into_value(span)?;
let json_value = value_to_json_value(&value)?; let json_value = value_to_json_value(engine_state, &value, serialize_types)?;
let json_result = if raw { let json_result = if raw {
nu_json::to_string_raw(&json_value) nu_json::to_string_raw(&json_value)
@ -105,7 +111,11 @@ impl Command for ToJson {
} }
} }
pub fn value_to_json_value(v: &Value) -> Result<nu_json::Value, ShellError> { pub fn value_to_json_value(
engine_state: &EngineState,
v: &Value,
serialize_types: bool,
) -> Result<nu_json::Value, ShellError> {
let span = v.span(); let span = v.span();
Ok(match v { Ok(match v {
Value::Bool { val, .. } => nu_json::Value::Bool(*val), Value::Bool { val, .. } => nu_json::Value::Bool(*val),
@ -127,31 +137,57 @@ pub fn value_to_json_value(v: &Value) -> Result<nu_json::Value, ShellError> {
.collect::<Result<Vec<nu_json::Value>, ShellError>>()?, .collect::<Result<Vec<nu_json::Value>, ShellError>>()?,
), ),
Value::List { vals, .. } => nu_json::Value::Array(json_list(vals)?), Value::List { vals, .. } => {
nu_json::Value::Array(json_list(engine_state, vals, serialize_types)?)
}
Value::Error { error, .. } => return Err(*error.clone()), Value::Error { error, .. } => return Err(*error.clone()),
Value::Closure { .. } | Value::Range { .. } => nu_json::Value::Null, Value::Closure { val, .. } => {
if serialize_types {
let block = engine_state.get_block(val.block_id);
if let Some(span) = block.span {
let contents_bytes = engine_state.get_span_contents(span);
let contents_string = String::from_utf8_lossy(contents_bytes);
nu_json::Value::String(contents_string.to_string())
} else {
nu_json::Value::String(format!(
"unable to retrieve block contents for json block_id {}",
val.block_id.get()
))
}
} else {
nu_json::Value::Null
}
}
Value::Range { .. } => nu_json::Value::Null,
Value::Binary { val, .. } => { Value::Binary { val, .. } => {
nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect()) nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect())
} }
Value::Record { val, .. } => { Value::Record { val, .. } => {
let mut m = nu_json::Map::new(); let mut m = nu_json::Map::new();
for (k, v) in &**val { for (k, v) in &**val {
m.insert(k.clone(), value_to_json_value(v)?); m.insert(
k.clone(),
value_to_json_value(engine_state, v, serialize_types)?,
);
} }
nu_json::Value::Object(m) nu_json::Value::Object(m)
} }
Value::Custom { val, .. } => { Value::Custom { val, .. } => {
let collected = val.to_base_value(span)?; let collected = val.to_base_value(span)?;
value_to_json_value(&collected)? value_to_json_value(engine_state, &collected, serialize_types)?
} }
}) })
} }
fn json_list(input: &[Value]) -> Result<Vec<nu_json::Value>, ShellError> { fn json_list(
engine_state: &EngineState,
input: &[Value],
serialize_types: bool,
) -> Result<Vec<nu_json::Value>, ShellError> {
let mut out = vec![]; let mut out = vec![];
for value in input { for value in input {
out.push(value_to_json_value(value)?); out.push(value_to_json_value(engine_state, value, serialize_types)?);
} }
Ok(out) Ok(out)

View File

@ -28,6 +28,11 @@ impl Command for ToNuon {
"specify indentation tab quantity", "specify indentation tab quantity",
Some('t'), Some('t'),
) )
.switch(
"serialize",
"serialize nushell types that cannot be deserialized",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -47,6 +52,7 @@ impl Command for ToNuon {
.unwrap_or_default() .unwrap_or_default()
.with_content_type(Some("application/x-nuon".into())); .with_content_type(Some("application/x-nuon".into()));
let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
let style = if call.has_flag(engine_state, stack, "raw")? { let style = if call.has_flag(engine_state, stack, "raw")? {
nuon::ToStyle::Raw nuon::ToStyle::Raw
} else if let Some(t) = call.get_flag(engine_state, stack, "tabs")? { } else if let Some(t) = call.get_flag(engine_state, stack, "tabs")? {
@ -60,7 +66,7 @@ impl Command for ToNuon {
let span = call.head; let span = call.head;
let value = input.into_value(span)?; let value = input.into_value(span)?;
match nuon::to_nuon(&value, style, Some(span)) { match nuon::to_nuon(engine_state, &value, style, Some(span), serialize_types) {
Ok(serde_nuon_string) => Ok(Value::string(serde_nuon_string, span) Ok(serde_nuon_string) => Ok(Value::string(serde_nuon_string, span)
.into_pipeline_data_with_metadata(Some(metadata))), .into_pipeline_data_with_metadata(Some(metadata))),
_ => Ok(Value::error( _ => Ok(Value::error(

View File

@ -27,6 +27,11 @@ impl Command for ToText {
"Do not append a newline to the end of the text", "Do not append a newline to the end of the text",
Some('n'), Some('n'),
) )
.switch(
"serialize",
"serialize nushell types that cannot be deserialized",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -43,6 +48,7 @@ impl Command for ToText {
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let no_newline = call.has_flag(engine_state, stack, "no-newline")?; let no_newline = call.has_flag(engine_state, stack, "no-newline")?;
let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
let input = input.try_expand_range()?; let input = input.try_expand_range()?;
let config = stack.get_config(engine_state); let config = stack.get_config(engine_state);
@ -56,7 +62,8 @@ impl Command for ToText {
Value::Record { val, .. } => !val.is_empty(), Value::Record { val, .. } => !val.is_empty(),
_ => false, _ => false,
}; };
let mut str = local_into_string(value, LINE_ENDING, &config); let mut str =
local_into_string(engine_state, value, LINE_ENDING, &config, serialize_types);
if add_trailing { if add_trailing {
str.push_str(LINE_ENDING); str.push_str(LINE_ENDING);
} }
@ -70,6 +77,7 @@ impl Command for ToText {
let stream = if no_newline { let stream = if no_newline {
let mut first = true; let mut first = true;
let mut iter = stream.into_inner(); let mut iter = stream.into_inner();
let engine_state_clone = engine_state.clone();
ByteStream::from_fn( ByteStream::from_fn(
span, span,
engine_state.signals().clone(), engine_state.signals().clone(),
@ -85,15 +93,28 @@ impl Command for ToText {
} }
// TODO: write directly into `buf` instead of creating an intermediate // TODO: write directly into `buf` instead of creating an intermediate
// string. // string.
let str = local_into_string(val, LINE_ENDING, &config); let str = local_into_string(
&engine_state_clone,
val,
LINE_ENDING,
&config,
serialize_types,
);
write!(buf, "{str}").err_span(head)?; write!(buf, "{str}").err_span(head)?;
Ok(true) Ok(true)
}, },
) )
} else { } else {
let engine_state_clone = engine_state.clone();
ByteStream::from_iter( ByteStream::from_iter(
stream.into_inner().map(move |val| { stream.into_inner().map(move |val| {
let mut str = local_into_string(val, LINE_ENDING, &config); let mut str = local_into_string(
&engine_state_clone,
val,
LINE_ENDING,
&config,
serialize_types,
);
str.push_str(LINE_ENDING); str.push_str(LINE_ENDING);
str str
}), }),
@ -137,7 +158,13 @@ impl Command for ToText {
} }
} }
fn local_into_string(value: Value, separator: &str, config: &Config) -> String { fn local_into_string(
engine_state: &EngineState,
value: Value,
separator: &str,
config: &Config,
serialize_types: bool,
) -> String {
let span = value.span(); let span = value.span();
match value { match value {
Value::Bool { val, .. } => val.to_string(), Value::Bool { val, .. } => val.to_string(),
@ -153,16 +180,38 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
Value::Glob { val, .. } => val, Value::Glob { val, .. } => val,
Value::List { vals: val, .. } => val Value::List { vals: val, .. } => val
.into_iter() .into_iter()
.map(|x| local_into_string(x, ", ", config)) .map(|x| local_into_string(engine_state, x, ", ", config, serialize_types))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(separator), .join(separator),
Value::Record { val, .. } => val Value::Record { val, .. } => val
.into_owned() .into_owned()
.into_iter() .into_iter()
.map(|(x, y)| format!("{}: {}", x, local_into_string(y, ", ", config))) .map(|(x, y)| {
format!(
"{}: {}",
x,
local_into_string(engine_state, y, ", ", config, serialize_types)
)
})
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(separator), .join(separator),
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id.get()), Value::Closure { val, .. } => {
if serialize_types {
let block = engine_state.get_block(val.block_id);
if let Some(span) = block.span {
let contents_bytes = engine_state.get_span_contents(span);
let contents_string = String::from_utf8_lossy(contents_bytes);
contents_string.to_string()
} else {
format!(
"unable to retrieve block contents for text block_id {}",
val.block_id.get()
)
}
} else {
format!("closure_{}", val.block_id.get())
}
}
Value::Nothing { .. } => String::new(), Value::Nothing { .. } => String::new(),
Value::Error { error, .. } => format!("{error:?}"), Value::Error { error, .. } => format!("{error:?}"),
Value::Binary { val, .. } => format!("{val:?}"), Value::Binary { val, .. } => format!("{val:?}"),
@ -171,7 +220,7 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
// that critical here // that critical here
Value::Custom { val, .. } => val Value::Custom { val, .. } => val
.to_base_value(span) .to_base_value(span)
.map(|val| local_into_string(val, separator, config)) .map(|val| local_into_string(engine_state, val, separator, config, serialize_types))
.unwrap_or_else(|_| format!("<{}>", val.type_name())), .unwrap_or_else(|_| format!("<{}>", val.type_name())),
} }
} }

View File

@ -13,6 +13,11 @@ impl Command for ToToml {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("to toml") Signature::build("to toml")
.input_output_types(vec![(Type::record(), Type::String)]) .input_output_types(vec![(Type::record(), Type::String)])
.switch(
"serialize",
"serialize nushell types that cannot be deserialized",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -31,19 +36,24 @@ impl Command for ToToml {
fn run( fn run(
&self, &self,
engine_state: &EngineState, engine_state: &EngineState,
_stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
to_toml(engine_state, input, head) let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
to_toml(engine_state, input, head, serialize_types)
} }
} }
// Helper method to recursively convert nu_protocol::Value -> toml::Value // Helper method to recursively convert nu_protocol::Value -> toml::Value
// This shouldn't be called at the top-level // This shouldn't be called at the top-level
fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellError> { fn helper(
let span = v.span(); engine_state: &EngineState,
v: &Value,
serialize_types: bool,
) -> Result<toml::Value, ShellError> {
Ok(match &v { Ok(match &v {
Value::Bool { val, .. } => toml::Value::Boolean(*val), Value::Bool { val, .. } => toml::Value::Boolean(*val),
Value::Int { val, .. } => toml::Value::Integer(*val), Value::Int { val, .. } => toml::Value::Integer(*val),
@ -56,15 +66,29 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
Value::Record { val, .. } => { Value::Record { val, .. } => {
let mut m = toml::map::Map::new(); let mut m = toml::map::Map::new();
for (k, v) in &**val { for (k, v) in &**val {
m.insert(k.clone(), helper(engine_state, v)?); m.insert(k.clone(), helper(engine_state, v, serialize_types)?);
} }
toml::Value::Table(m) toml::Value::Table(m)
} }
Value::List { vals, .. } => toml::Value::Array(toml_list(engine_state, vals)?), Value::List { vals, .. } => {
Value::Closure { .. } => { toml::Value::Array(toml_list(engine_state, vals, serialize_types)?)
let code = engine_state.get_span_contents(span); }
let code = String::from_utf8_lossy(code).to_string(); Value::Closure { val, .. } => {
toml::Value::String(code) if serialize_types {
let block = engine_state.get_block(val.block_id);
if let Some(span) = block.span {
let contents_bytes = engine_state.get_span_contents(span);
let contents_string = String::from_utf8_lossy(contents_bytes);
toml::Value::String(contents_string.to_string())
} else {
toml::Value::String(format!(
"unable to retrieve block contents for toml block_id {}",
val.block_id.get()
))
}
} else {
toml::Value::String(format!("closure_{}", val.block_id.get()))
}
} }
Value::Nothing { .. } => toml::Value::String("<Nothing>".to_string()), Value::Nothing { .. } => toml::Value::String("<Nothing>".to_string()),
Value::Error { error, .. } => return Err(*error.clone()), Value::Error { error, .. } => return Err(*error.clone()),
@ -86,11 +110,15 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
}) })
} }
fn toml_list(engine_state: &EngineState, input: &[Value]) -> Result<Vec<toml::Value>, ShellError> { fn toml_list(
engine_state: &EngineState,
input: &[Value],
serialize_types: bool,
) -> Result<Vec<toml::Value>, ShellError> {
let mut out = vec![]; let mut out = vec![];
for value in input { for value in input {
out.push(helper(engine_state, value)?); out.push(helper(engine_state, value, serialize_types)?);
} }
Ok(out) Ok(out)
@ -129,9 +157,10 @@ fn value_to_toml_value(
engine_state: &EngineState, engine_state: &EngineState,
v: &Value, v: &Value,
head: Span, head: Span,
serialize_types: bool,
) -> Result<toml::Value, ShellError> { ) -> Result<toml::Value, ShellError> {
match v { match v {
Value::Record { .. } => helper(engine_state, v), Value::Record { .. } | Value::Closure { .. } => helper(engine_state, v, serialize_types),
// Propagate existing errors // Propagate existing errors
Value::Error { error, .. } => Err(*error.clone()), Value::Error { error, .. } => Err(*error.clone()),
_ => Err(ShellError::UnsupportedInput { _ => Err(ShellError::UnsupportedInput {
@ -147,11 +176,12 @@ fn to_toml(
engine_state: &EngineState, engine_state: &EngineState,
input: PipelineData, input: PipelineData,
span: Span, span: Span,
serialize_types: bool,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let metadata = input.metadata(); let metadata = input.metadata();
let value = input.into_value(span)?; let value = input.into_value(span)?;
let toml_value = value_to_toml_value(engine_state, &value, span)?; let toml_value = value_to_toml_value(engine_state, &value, span, serialize_types)?;
match toml_value { match toml_value {
toml::Value::Array(ref vec) => match vec[..] { toml::Value::Array(ref vec) => match vec[..] {
[toml::Value::Table(_)] => toml_into_pipeline_data( [toml::Value::Table(_)] => toml_into_pipeline_data(
@ -218,6 +248,7 @@ mod tests {
#[test] #[test]
fn to_toml_creates_correct_date() { fn to_toml_creates_correct_date() {
let engine_state = EngineState::new(); let engine_state = EngineState::new();
let serialize_types = false;
let test_date = Value::date( let test_date = Value::date(
chrono::FixedOffset::east_opt(60 * 120) chrono::FixedOffset::east_opt(60 * 120)
@ -242,7 +273,7 @@ mod tests {
offset: Some(toml::value::Offset::Custom { minutes: 120 }), offset: Some(toml::value::Offset::Custom { minutes: 120 }),
}); });
let result = helper(&engine_state, &test_date); let result = helper(&engine_state, &test_date, serialize_types);
assert!(result.is_ok_and(|res| res == reference_date)); assert!(result.is_ok_and(|res| res == reference_date));
} }
@ -254,6 +285,7 @@ mod tests {
// //
let engine_state = EngineState::new(); let engine_state = EngineState::new();
let serialize_types = false;
let mut m = indexmap::IndexMap::new(); let mut m = indexmap::IndexMap::new();
m.insert("rust".to_owned(), Value::test_string("editor")); m.insert("rust".to_owned(), Value::test_string("editor"));
@ -269,6 +301,7 @@ mod tests {
&engine_state, &engine_state,
&Value::record(m.into_iter().collect(), Span::test_data()), &Value::record(m.into_iter().collect(), Span::test_data()),
Span::test_data(), Span::test_data(),
serialize_types,
) )
.expect("Expected Ok from valid TOML dictionary"); .expect("Expected Ok from valid TOML dictionary");
assert_eq!( assert_eq!(
@ -285,12 +318,14 @@ mod tests {
&engine_state, &engine_state,
&Value::test_string("not_valid"), &Value::test_string("not_valid"),
Span::test_data(), Span::test_data(),
serialize_types,
) )
.expect_err("Expected non-valid toml (String) to cause error!"); .expect_err("Expected non-valid toml (String) to cause error!");
value_to_toml_value( value_to_toml_value(
&engine_state, &engine_state,
&Value::list(vec![Value::test_string("1")], Span::test_data()), &Value::list(vec![Value::test_string("1")], Span::test_data()),
Span::test_data(), Span::test_data(),
serialize_types,
) )
.expect_err("Expected non-valid toml (Table) to cause error!"); .expect_err("Expected non-valid toml (Table) to cause error!");
} }

View File

@ -12,6 +12,11 @@ impl Command for ToYaml {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("to yaml") Signature::build("to yaml")
.input_output_types(vec![(Type::Any, Type::String)]) .input_output_types(vec![(Type::Any, Type::String)])
.switch(
"serialize",
"serialize nushell types that cannot be deserialized",
Some('s'),
)
.category(Category::Formats) .category(Category::Formats)
} }
@ -29,18 +34,24 @@ impl Command for ToYaml {
fn run( fn run(
&self, &self,
_engine_state: &EngineState, engine_state: &EngineState,
_stack: &mut Stack, stack: &mut Stack,
call: &Call, call: &Call,
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
let input = input.try_expand_range()?; let input = input.try_expand_range()?;
to_yaml(input, head)
to_yaml(engine_state, input, head, serialize_types)
} }
} }
pub fn value_to_yaml_value(v: &Value) -> Result<serde_yml::Value, ShellError> { pub fn value_to_yaml_value(
engine_state: &EngineState,
v: &Value,
serialize_types: bool,
) -> Result<serde_yml::Value, ShellError> {
Ok(match &v { Ok(match &v {
Value::Bool { val, .. } => serde_yml::Value::Bool(*val), Value::Bool { val, .. } => serde_yml::Value::Bool(*val),
Value::Int { val, .. } => serde_yml::Value::Number(serde_yml::Number::from(*val)), Value::Int { val, .. } => serde_yml::Value::Number(serde_yml::Number::from(*val)),
@ -55,7 +66,10 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yml::Value, ShellError> {
Value::Record { val, .. } => { Value::Record { val, .. } => {
let mut m = serde_yml::Mapping::new(); let mut m = serde_yml::Mapping::new();
for (k, v) in &**val { for (k, v) in &**val {
m.insert(serde_yml::Value::String(k.clone()), value_to_yaml_value(v)?); m.insert(
serde_yml::Value::String(k.clone()),
value_to_yaml_value(engine_state, v, serialize_types)?,
);
} }
serde_yml::Value::Mapping(m) serde_yml::Value::Mapping(m)
} }
@ -63,12 +77,28 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yml::Value, ShellError> {
let mut out = vec![]; let mut out = vec![];
for value in vals { for value in vals {
out.push(value_to_yaml_value(value)?); out.push(value_to_yaml_value(engine_state, value, serialize_types)?);
} }
serde_yml::Value::Sequence(out) serde_yml::Value::Sequence(out)
} }
Value::Closure { .. } => serde_yml::Value::Null, Value::Closure { val, .. } => {
if serialize_types {
let block = engine_state.get_block(val.block_id);
if let Some(span) = block.span {
let contents_bytes = engine_state.get_span_contents(span);
let contents_string = String::from_utf8_lossy(contents_bytes);
serde_yml::Value::String(contents_string.to_string())
} else {
serde_yml::Value::String(format!(
"unable to retrieve block contents for yaml block_id {}",
val.block_id.get()
))
}
} else {
serde_yml::Value::Null
}
}
Value::Nothing { .. } => serde_yml::Value::Null, Value::Nothing { .. } => serde_yml::Value::Null,
Value::Error { error, .. } => return Err(*error.clone()), Value::Error { error, .. } => return Err(*error.clone()),
Value::Binary { val, .. } => serde_yml::Value::Sequence( Value::Binary { val, .. } => serde_yml::Value::Sequence(
@ -91,7 +121,12 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yml::Value, ShellError> {
}) })
} }
fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> { fn to_yaml(
engine_state: &EngineState,
input: PipelineData,
head: Span,
serialize_types: bool,
) -> Result<PipelineData, ShellError> {
let metadata = input let metadata = input
.metadata() .metadata()
.unwrap_or_default() .unwrap_or_default()
@ -99,7 +134,7 @@ fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
.with_content_type(Some("application/yaml".into())); .with_content_type(Some("application/yaml".into()));
let value = input.into_value(head)?; let value = input.into_value(head)?;
let yaml_value = value_to_yaml_value(&value)?; let yaml_value = value_to_yaml_value(engine_state, &value, serialize_types)?;
match serde_yml::to_string(&yaml_value) { match serde_yml::to_string(&yaml_value) {
Ok(serde_yml_string) => { Ok(serde_yml_string) => {
Ok(Value::string(serde_yml_string, head) Ok(Value::string(serde_yml_string, head)
@ -120,11 +155,9 @@ fn to_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError>
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
use crate::{Get, Metadata};
use super::*; use super::*;
use crate::{Get, Metadata};
use nu_cmd_lang::eval_pipeline_without_terminal_expression;
#[test] #[test]
fn test_examples() { fn test_examples() {

View File

@ -205,6 +205,7 @@ pub enum HttpBody {
// remove once all commands have been migrated // remove once all commands have been migrated
pub fn send_request( pub fn send_request(
engine_state: &EngineState,
request: Request, request: Request,
http_body: HttpBody, http_body: HttpBody,
content_type: Option<String>, content_type: Option<String>,
@ -212,6 +213,9 @@ pub fn send_request(
signals: &Signals, signals: &Signals,
) -> Result<Response, ShellErrorOrRequestError> { ) -> Result<Response, ShellErrorOrRequestError> {
let request_url = request.url().to_string(); let request_url = request.url().to_string();
// hard code serialze_types to false because closures probably shouldn't be
// deserialized for send_request but it's required by send_json_request
let serialze_types = false;
match http_body { match http_body {
HttpBody::None => { HttpBody::None => {
@ -238,7 +242,15 @@ pub fn send_request(
}; };
match body_type { match body_type {
BodyType::Json => send_json_request(&request_url, body, req, span, signals), BodyType::Json => send_json_request(
engine_state,
&request_url,
body,
req,
span,
signals,
serialze_types,
),
BodyType::Form => send_form_request(&request_url, body, req, span, signals), BodyType::Form => send_form_request(&request_url, body, req, span, signals),
BodyType::Multipart => { BodyType::Multipart => {
send_multipart_request(&request_url, body, req, span, signals) send_multipart_request(&request_url, body, req, span, signals)
@ -252,15 +264,17 @@ pub fn send_request(
} }
fn send_json_request( fn send_json_request(
engine_state: &EngineState,
request_url: &str, request_url: &str,
body: Value, body: Value,
req: Request, req: Request,
span: Span, span: Span,
signals: &Signals, signals: &Signals,
serialize_types: bool,
) -> Result<Response, ShellErrorOrRequestError> { ) -> Result<Response, ShellErrorOrRequestError> {
match body { match body {
Value::Int { .. } | Value::Float { .. } | Value::List { .. } | Value::Record { .. } => { Value::Int { .. } | Value::Float { .. } | Value::List { .. } | Value::Record { .. } => {
let data = value_to_json_value(&body)?; let data = value_to_json_value(engine_state, &body, serialize_types)?;
send_cancellable_request(request_url, Box::new(|| req.send_json(data)), span, signals) send_cancellable_request(request_url, Box::new(|| req.send_json(data)), span, signals)
} }
// If the body type is string, assume it is string json content. // If the body type is string, assume it is string json content.

View File

@ -214,6 +214,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
args.data, args.data,
args.content_type, args.content_type,

View File

@ -182,6 +182,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
HttpBody::None, HttpBody::None,
None, None,

View File

@ -155,7 +155,14 @@ fn helper(
request = request_add_authorization_header(args.user, args.password, request); request = request_add_authorization_header(args.user, args.password, request);
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request(request, HttpBody::None, None, call.head, signals); let response = send_request(
engine_state,
request,
HttpBody::None,
None,
call.head,
signals,
);
check_response_redirection(redirect_mode, span, &response)?; check_response_redirection(redirect_mode, span, &response)?;
request_handle_response_headers(span, response) request_handle_response_headers(span, response)
} }

View File

@ -161,6 +161,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
HttpBody::None, HttpBody::None,
None, None,

View File

@ -216,6 +216,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
args.data, args.data,
args.content_type, args.content_type,

View File

@ -224,6 +224,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
args.data, args.data,
args.content_type, args.content_type,

View File

@ -215,6 +215,7 @@ fn helper(
request = request_add_custom_headers(args.headers, request)?; request = request_add_custom_headers(args.headers, request)?;
let response = send_request( let response = send_request(
engine_state,
request.clone(), request.clone(),
args.data, args.data,
args.content_type, args.content_type,

View File

@ -1,6 +1,6 @@
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use nu_path::AbsolutePathBuf; use nu_path::AbsolutePathBuf;
use nu_protocol::{ast::PathMember, record, Span, Value}; use nu_protocol::{ast::PathMember, engine::EngineState, record, Span, Value};
use nu_test_support::{ use nu_test_support::{
fs::{line_ending, Stub}, fs::{line_ending, Stub},
nu, pipeline, nu, pipeline,
@ -298,6 +298,9 @@ fn into_sqlite_existing_db_append() {
/// streaming pipeline instead of a simple value /// streaming pipeline instead of a simple value
#[test] #[test]
fn into_sqlite_big_insert() { fn into_sqlite_big_insert() {
let engine_state = EngineState::new();
// don't serialize closures
let serialize_types = false;
Playground::setup("big_insert", |dirs, playground| { Playground::setup("big_insert", |dirs, playground| {
const NUM_ROWS: usize = 10_000; const NUM_ROWS: usize = 10_000;
const NUON_FILE_NAME: &str = "data.nuon"; const NUON_FILE_NAME: &str = "data.nuon";
@ -330,7 +333,14 @@ fn into_sqlite_big_insert() {
) )
.unwrap(); .unwrap();
let nuon = nuon::to_nuon(&value, nuon::ToStyle::Raw, Some(Span::unknown())).unwrap() let nuon = nuon::to_nuon(
&engine_state,
&value,
nuon::ToStyle::Raw,
Some(Span::unknown()),
serialize_types,
)
.unwrap()
+ &line_ending(); + &line_ending();
nuon_file.write_all(nuon.as_bytes()).unwrap(); nuon_file.write_all(nuon.as_bytes()).unwrap();

View File

@ -278,6 +278,7 @@ fn from_nuon_datetime() {
} }
#[test] #[test]
#[ignore]
fn to_nuon_errs_on_closure() { fn to_nuon_errs_on_closure() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"

View File

@ -908,7 +908,7 @@ impl Value {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(separator) .join(separator)
), ),
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id.get()), Value::Closure { val, .. } => format!("closure_{}", val.block_id.get()),
Value::Nothing { .. } => String::new(), Value::Nothing { .. } => String::new(),
Value::Error { error, .. } => format!("{error:?}"), Value::Error { error, .. } => format!("{error:?}"),
Value::Binary { val, .. } => format!("{val:?}"), Value::Binary { val, .. } => format!("{val:?}"),

View File

@ -11,7 +11,7 @@ mod tests {
use chrono::DateTime; use chrono::DateTime;
use nu_protocol::{ use nu_protocol::{
ast::{CellPath, PathMember, RangeInclusion}, ast::{CellPath, PathMember, RangeInclusion},
engine::Closure, engine::{Closure, EngineState},
record, BlockId, IntRange, Range, Span, Value, record, BlockId, IntRange, Range, Span, Value,
}; };
@ -25,11 +25,15 @@ mod tests {
/// an optional "middle" value can be given to test what the value is between `from nuon` and /// an optional "middle" value can be given to test what the value is between `from nuon` and
/// `to nuon`. /// `to nuon`.
fn nuon_end_to_end(input: &str, middle: Option<Value>) { fn nuon_end_to_end(input: &str, middle: Option<Value>) {
let engine_state = EngineState::new();
let val = from_nuon(input, None).unwrap(); let val = from_nuon(input, None).unwrap();
if let Some(m) = middle { if let Some(m) = middle {
assert_eq!(val, m); assert_eq!(val, m);
} }
assert_eq!(to_nuon(&val, ToStyle::Raw, None).unwrap(), input); assert_eq!(
to_nuon(&engine_state, &val, ToStyle::Raw, None, false).unwrap(),
input
);
} }
#[test] #[test]
@ -172,14 +176,19 @@ mod tests {
} }
#[test] #[test]
#[ignore]
fn to_nuon_errs_on_closure() { fn to_nuon_errs_on_closure() {
let engine_state = EngineState::new();
assert!(to_nuon( assert!(to_nuon(
&engine_state,
&Value::test_closure(Closure { &Value::test_closure(Closure {
block_id: BlockId::new(0), block_id: BlockId::new(0),
captures: vec![] captures: vec![]
}), }),
ToStyle::Raw, ToStyle::Raw,
None, None,
false,
) )
.unwrap_err() .unwrap_err()
.to_string() .to_string()
@ -196,8 +205,17 @@ mod tests {
#[test] #[test]
fn binary_roundtrip() { fn binary_roundtrip() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon(&from_nuon("0x[1f ff]", None).unwrap(), ToStyle::Raw, None).unwrap(), to_nuon(
&engine_state,
&from_nuon("0x[1f ff]", None).unwrap(),
ToStyle::Raw,
None,
false,
)
.unwrap(),
"0x[1FFF]" "0x[1FFF]"
); );
} }
@ -237,40 +255,79 @@ mod tests {
#[test] #[test]
fn float_doesnt_become_int() { fn float_doesnt_become_int() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon(&Value::test_float(1.0), ToStyle::Raw, None).unwrap(), to_nuon(
&engine_state,
&Value::test_float(1.0),
ToStyle::Raw,
None,
false
)
.unwrap(),
"1.0" "1.0"
); );
} }
#[test] #[test]
fn float_inf_parsed_properly() { fn float_inf_parsed_properly() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon(&Value::test_float(f64::INFINITY), ToStyle::Raw, None).unwrap(), to_nuon(
&engine_state,
&Value::test_float(f64::INFINITY),
ToStyle::Raw,
None,
false,
)
.unwrap(),
"inf" "inf"
); );
} }
#[test] #[test]
fn float_neg_inf_parsed_properly() { fn float_neg_inf_parsed_properly() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon(&Value::test_float(f64::NEG_INFINITY), ToStyle::Raw, None).unwrap(), to_nuon(
&engine_state,
&Value::test_float(f64::NEG_INFINITY),
ToStyle::Raw,
None,
false,
)
.unwrap(),
"-inf" "-inf"
); );
} }
#[test] #[test]
fn float_nan_parsed_properly() { fn float_nan_parsed_properly() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon(&Value::test_float(-f64::NAN), ToStyle::Raw, None).unwrap(), to_nuon(
&engine_state,
&Value::test_float(-f64::NAN),
ToStyle::Raw,
None,
false,
)
.unwrap(),
"NaN" "NaN"
); );
} }
#[test] #[test]
fn to_nuon_converts_columns_with_spaces() { fn to_nuon_converts_columns_with_spaces() {
let engine_state = EngineState::new();
assert!(from_nuon( assert!(from_nuon(
&to_nuon( &to_nuon(
&engine_state,
&Value::test_list(vec![ &Value::test_list(vec![
Value::test_record(record!( Value::test_record(record!(
"a" => Value::test_int(1), "a" => Value::test_int(1),
@ -284,7 +341,8 @@ mod tests {
)) ))
]), ]),
ToStyle::Raw, ToStyle::Raw,
None None,
false,
) )
.unwrap(), .unwrap(),
None, None,
@ -294,7 +352,15 @@ mod tests {
#[test] #[test]
fn to_nuon_quotes_empty_string() { fn to_nuon_quotes_empty_string() {
let res = to_nuon(&Value::test_string(""), ToStyle::Raw, None); let engine_state = EngineState::new();
let res = to_nuon(
&engine_state,
&Value::test_string(""),
ToStyle::Raw,
None,
false,
);
assert!(res.is_ok()); assert!(res.is_ok());
assert_eq!(res.unwrap(), r#""""#); assert_eq!(res.unwrap(), r#""""#);
} }
@ -340,8 +406,11 @@ mod tests {
#[test] #[test]
fn does_not_quote_strings_unnecessarily() { fn does_not_quote_strings_unnecessarily() {
let engine_state = EngineState::new();
assert_eq!( assert_eq!(
to_nuon( to_nuon(
&engine_state,
&Value::test_list(vec![ &Value::test_list(vec![
Value::test_record(record!( Value::test_record(record!(
"a" => Value::test_int(1), "a" => Value::test_int(1),
@ -355,7 +424,8 @@ mod tests {
)) ))
]), ]),
ToStyle::Raw, ToStyle::Raw,
None None,
false,
) )
.unwrap(), .unwrap(),
"[[a, b, \"c d\"]; [1, 2, 3], [4, 5, 6]]" "[[a, b, \"c d\"]; [1, 2, 3], [4, 5, 6]]"
@ -363,12 +433,14 @@ mod tests {
assert_eq!( assert_eq!(
to_nuon( to_nuon(
&engine_state,
&Value::test_record(record!( &Value::test_record(record!(
"ro name" => Value::test_string("sam"), "ro name" => Value::test_string("sam"),
"rank" => Value::test_int(10) "rank" => Value::test_int(10)
)), )),
ToStyle::Raw, ToStyle::Raw,
None None,
false,
) )
.unwrap(), .unwrap(),
"{\"ro name\": sam, rank: 10}" "{\"ro name\": sam, rank: 10}"

View File

@ -1,7 +1,6 @@
use core::fmt::Write; use core::fmt::Write;
use nu_engine::get_columns; use nu_engine::get_columns;
use nu_protocol::{Range, ShellError, Span, Value}; use nu_protocol::{engine::EngineState, Range, ShellError, Span, Value};
use nu_utils::{escape_quote_string, needs_quoting}; use nu_utils::{escape_quote_string, needs_quoting};
use std::ops::Bound; use std::ops::Bound;
@ -44,7 +43,13 @@ pub enum ToStyle {
/// > using this function in a command implementation such as [`to nuon`](https://www.nushell.sh/commands/docs/to_nuon.html). /// > using this function in a command implementation such as [`to nuon`](https://www.nushell.sh/commands/docs/to_nuon.html).
/// ///
/// also see [`super::from_nuon`] for the inverse operation /// also see [`super::from_nuon`] for the inverse operation
pub fn to_nuon(input: &Value, style: ToStyle, span: Option<Span>) -> Result<String, ShellError> { pub fn to_nuon(
engine_state: &EngineState,
input: &Value,
style: ToStyle,
span: Option<Span>,
serialize_types: bool,
) -> Result<String, ShellError> {
let span = span.unwrap_or(Span::unknown()); let span = span.unwrap_or(Span::unknown());
let indentation = match style { let indentation = match style {
@ -53,16 +58,25 @@ pub fn to_nuon(input: &Value, style: ToStyle, span: Option<Span>) -> Result<Stri
ToStyle::Spaces(s) => Some(" ".repeat(s)), ToStyle::Spaces(s) => Some(" ".repeat(s)),
}; };
let res = value_to_string(input, span, 0, indentation.as_deref())?; let res = value_to_string(
engine_state,
input,
span,
0,
indentation.as_deref(),
serialize_types,
)?;
Ok(res) Ok(res)
} }
fn value_to_string( fn value_to_string(
engine_state: &EngineState,
v: &Value, v: &Value,
span: Span, span: Span,
depth: usize, depth: usize,
indent: Option<&str>, indent: Option<&str>,
serialize_types: bool,
) -> Result<String, ShellError> { ) -> Result<String, ShellError> {
let (nl, sep) = get_true_separators(indent); let (nl, sep) = get_true_separators(indent);
let idt = get_true_indentation(depth, indent); let idt = get_true_indentation(depth, indent);
@ -84,12 +98,25 @@ fn value_to_string(
} }
Ok(format!("0x[{s}]")) Ok(format!("0x[{s}]"))
} }
Value::Closure { .. } => Err(ShellError::UnsupportedInput { Value::Closure { val, .. } => {
msg: "closures are currently not nuon-compatible".into(), if serialize_types {
input: "value originates from here".into(), let block = engine_state.get_block(val.block_id);
msg_span: span, if let Some(span) = block.span {
input_span: v.span(), let contents_bytes = engine_state.get_span_contents(span);
}), let contents_string = String::from_utf8_lossy(contents_bytes);
Ok(contents_string.to_string())
} else {
Ok(String::new())
}
} else {
Err(ShellError::UnsupportedInput {
msg: "closures are currently not nuon-compatible".into(),
input: "value originates from here".into(),
msg_span: span,
input_span: v.span(),
})
}
}
Value::Bool { val, .. } => { Value::Bool { val, .. } => {
if *val { if *val {
Ok("true".to_string()) Ok("true".to_string())
@ -144,10 +171,12 @@ fn value_to_string(
if let Value::Record { val, .. } = val { if let Value::Record { val, .. } = val {
for val in val.values() { for val in val.values() {
row.push(value_to_string_without_quotes( row.push(value_to_string_without_quotes(
engine_state,
val, val,
span, span,
depth + 2, depth + 2,
indent, indent,
serialize_types,
)?); )?);
} }
} }
@ -165,7 +194,14 @@ fn value_to_string(
for val in vals { for val in vals {
collection.push(format!( collection.push(format!(
"{idt_po}{}", "{idt_po}{}",
value_to_string_without_quotes(val, span, depth + 1, indent,)? value_to_string_without_quotes(
engine_state,
val,
span,
depth + 1,
indent,
serialize_types
)?
)); ));
} }
Ok(format!( Ok(format!(
@ -178,18 +214,38 @@ fn value_to_string(
Value::Range { val, .. } => match **val { Value::Range { val, .. } => match **val {
Range::IntRange(range) => Ok(range.to_string()), Range::IntRange(range) => Ok(range.to_string()),
Range::FloatRange(range) => { Range::FloatRange(range) => {
let start = let start = value_to_string(
value_to_string(&Value::float(range.start(), span), span, depth + 1, indent)?; engine_state,
&Value::float(range.start(), span),
span,
depth + 1,
indent,
serialize_types,
)?;
match range.end() { match range.end() {
Bound::Included(end) => Ok(format!( Bound::Included(end) => Ok(format!(
"{}..{}", "{}..{}",
start, start,
value_to_string(&Value::float(end, span), span, depth + 1, indent)? value_to_string(
engine_state,
&Value::float(end, span),
span,
depth + 1,
indent,
serialize_types,
)?
)), )),
Bound::Excluded(end) => Ok(format!( Bound::Excluded(end) => Ok(format!(
"{}..<{}", "{}..<{}",
start, start,
value_to_string(&Value::float(end, span), span, depth + 1, indent)? value_to_string(
engine_state,
&Value::float(end, span),
span,
depth + 1,
indent,
serialize_types,
)?
)), )),
Bound::Unbounded => Ok(format!("{start}..",)), Bound::Unbounded => Ok(format!("{start}..",)),
} }
@ -205,7 +261,14 @@ fn value_to_string(
}; };
collection.push(format!( collection.push(format!(
"{idt_po}{col}: {}", "{idt_po}{col}: {}",
value_to_string_without_quotes(val, span, depth + 1, indent)? value_to_string_without_quotes(
engine_state,
val,
span,
depth + 1,
indent,
serialize_types
)?
)); ));
} }
Ok(format!( Ok(format!(
@ -235,10 +298,12 @@ fn get_true_separators(indent: Option<&str>) -> (String, String) {
} }
fn value_to_string_without_quotes( fn value_to_string_without_quotes(
engine_state: &EngineState,
v: &Value, v: &Value,
span: Span, span: Span,
depth: usize, depth: usize,
indent: Option<&str>, indent: Option<&str>,
serialize_types: bool,
) -> Result<String, ShellError> { ) -> Result<String, ShellError> {
match v { match v {
Value::String { val, .. } => Ok({ Value::String { val, .. } => Ok({
@ -248,6 +313,6 @@ fn value_to_string_without_quotes(
val.clone() val.clone()
} }
}), }),
_ => value_to_string(v, span, depth, indent), _ => value_to_string(engine_state, v, span, depth, indent, serialize_types),
} }
} }

View File

@ -103,7 +103,27 @@ fn literal_binary() {
#[test] #[test]
fn literal_closure() { fn literal_closure() {
test_eval("{||}", Matches("<Closure")) test_eval("{||}", Matches("closure_"))
}
#[test]
fn literal_closure_to_nuon() {
test_eval("{||} | to nuon --serialize", Eq("{||}"))
}
#[test]
fn literal_closure_to_json() {
test_eval("{||} | to json --serialize", Eq("\"{||}\""))
}
#[test]
fn literal_closure_to_toml() {
test_eval("{a: {||}} | to toml --serialize", Eq("a = \"{||}\""))
}
#[test]
fn literal_closure_to_yaml() {
test_eval("{||} | to yaml --serialize", Eq("'{||}'"))
} }
#[test] #[test]