From 624edce4f75f9ce01587fe29e21d8e823371ec77 Mon Sep 17 00:00:00 2001 From: JT Date: Fri, 29 Oct 2021 19:26:29 +1300 Subject: [PATCH] Add 'to json' --- crates/nu-command/src/default_context.rs | 2 + crates/nu-command/src/example_test.rs | 3 + crates/nu-command/src/formats/mod.rs | 2 + crates/nu-command/src/formats/to/command.rs | 30 +++ crates/nu-command/src/formats/to/json.rs | 235 ++++++++++++++++++++ crates/nu-command/src/formats/to/mod.rs | 5 + crates/nu-json/src/ser.rs | 27 ++- 7 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 crates/nu-command/src/formats/to/command.rs create mode 100644 crates/nu-command/src/formats/to/json.rs create mode 100644 crates/nu-command/src/formats/to/mod.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 93e9e49f9..77993b7e5 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -67,6 +67,8 @@ pub fn create_default_context() -> EngineState { SplitRow, Sys, Table, + To, + ToJson, Touch, Use, Where, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 3685af399..f5879d5c9 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -5,6 +5,8 @@ use nu_protocol::{ PipelineData, }; +use crate::To; + use super::{From, Into, Math, Split}; pub fn test_examples(cmd: impl Command + 'static) { @@ -16,6 +18,7 @@ pub fn test_examples(cmd: impl Command + 'static) { // Try to keep this working set small to keep tests running as fast as possible let mut working_set = StateWorkingSet::new(&*engine_state); working_set.add_decl(Box::new(From)); + working_set.add_decl(Box::new(To)); working_set.add_decl(Box::new(Into)); working_set.add_decl(Box::new(Split)); working_set.add_decl(Box::new(Math)); diff --git a/crates/nu-command/src/formats/mod.rs b/crates/nu-command/src/formats/mod.rs index ad1e00563..86f06f85f 100644 --- a/crates/nu-command/src/formats/mod.rs +++ b/crates/nu-command/src/formats/mod.rs @@ -1,3 +1,5 @@ mod from; +mod to; pub use from::*; +pub use to::*; diff --git a/crates/nu-command/src/formats/to/command.rs b/crates/nu-command/src/formats/to/command.rs new file mode 100644 index 000000000..ebc5e6b8c --- /dev/null +++ b/crates/nu-command/src/formats/to/command.rs @@ -0,0 +1,30 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct To; + +impl Command for To { + fn name(&self) -> &str { + "to" + } + + fn usage(&self) -> &str { + "Translate structured data to a format" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("to") + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new()) + } +} diff --git a/crates/nu-command/src/formats/to/json.rs b/crates/nu-command/src/formats/to/json.rs new file mode 100644 index 000000000..952c029bd --- /dev/null +++ b/crates/nu-command/src/formats/to/json.rs @@ -0,0 +1,235 @@ +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, + Value, +}; + +#[derive(Clone)] +pub struct ToJson; + +impl Command for ToJson { + fn name(&self) -> &str { + "to json" + } + + fn signature(&self) -> Signature { + Signature::build("to json") + // .named( + // "pretty", + // SyntaxShape::Int, + // "Formats the JSON text with the provided indentation setting", + // Some('p'), + // ) + } + + fn usage(&self) -> &str { + "Converts table data into JSON text." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + to_json(engine_state, call, input) + } + + fn examples(&self) -> Vec { + vec![Example { + description: + "Outputs an unformatted JSON string representing the contents of this table", + example: "[1 2 3] | to json", + result: Some(Value::test_string("[\n 1,\n 2,\n 3\n]")), + }] + } +} + +pub fn value_to_json_value(v: &Value) -> Result { + Ok(match v { + Value::Bool { val, .. } => nu_json::Value::Bool(*val), + Value::Filesize { val, .. } => nu_json::Value::I64(*val), + Value::Duration { val, .. } => nu_json::Value::I64(*val), + Value::Date { val, .. } => nu_json::Value::String(val.to_string()), + Value::Float { val, .. } => nu_json::Value::F64(*val), + Value::Int { val, .. } => nu_json::Value::I64(*val), + Value::Nothing { .. } => nu_json::Value::Null, + Value::String { val, .. } => nu_json::Value::String(val.to_string()), + Value::CellPath { val, .. } => nu_json::Value::Array( + val.members + .iter() + .map(|x| match &x { + PathMember::String { val, .. } => Ok(nu_json::Value::String(val.clone())), + PathMember::Int { val, .. } => Ok(nu_json::Value::U64(*val as u64)), + }) + .collect::, ShellError>>()?, + ), + + Value::List { vals, .. } => nu_json::Value::Array(json_list(vals)?), + Value::Error { error } => return Err(error.clone()), + Value::Block { .. } | Value::Range { .. } => nu_json::Value::Null, + #[cfg(feature = "dataframe")] + UntaggedValue::DataFrame(_) | UntaggedValue::FrameStruct(_) => serde_json::Value::Null, + Value::Binary { val, .. } => { + nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect()) + } + Value::Record { cols, vals, .. } => { + let mut m = nu_json::Map::new(); + for (k, v) in cols.iter().zip(vals) { + m.insert(k.clone(), value_to_json_value(v)?); + } + nu_json::Value::Object(m) + } + }) +} + +fn json_list(input: &[Value]) -> Result, ShellError> { + let mut out = vec![]; + + for value in input { + out.push(value_to_json_value(value)?); + } + + Ok(out) +} + +fn to_json( + engine_state: &EngineState, + call: &Call, + input: PipelineData, +) -> Result { + let name_span = call.head; + // let pretty: Option = args.get_flag("pretty")?; + + //let input: Vec = input.collect(); + + // let to_process_input = match input.len() { + // x if x > 1 => { + // let tag = input[0].tag.clone(); + // vec![Value:: { + // value: UntaggedValue::Table(input), + // tag, + // }] + // } + // 1 => input, + // _ => vec![], + // }; + + match input { + PipelineData::Value(value) => { + let json_value = value_to_json_value(&value)?; + match nu_json::to_string(&json_value) { + Ok(serde_json_string) => Ok(Value::String { + val: serde_json_string, + span: name_span, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("JSON".into(), name_span), + } + .into_pipeline_data()), + } + } + PipelineData::Stream(stream) => Ok(stream + .map(move |value| { + if let Ok(json_value) = value_to_json_value(&value) { + match nu_json::to_string(&json_value) { + Ok(serde_json_string) => Value::String { + val: serde_json_string, + span: name_span, + }, + _ => Value::Error { + error: ShellError::CantConvert("JSON".into(), name_span), + }, + } + } else { + Value::Error { + error: ShellError::CantConvert("JSON".into(), name_span), + } + } + }) + .into_pipeline_data(engine_state.ctrlc.clone())), + } + // input + // // .into_iter() + // .map( + // move |value| { + // let value_span = value.span().expect("non-error"); + // match value_to_json_value(&value) { + // Ok(json_value) => { + // match nu_json::to_string(&json_value) { + // Ok(serde_json_string) => { + // // if let Some(pretty_value) = &pretty { + // // let mut pretty_format_failed = true; + + // // if let Ok(pretty_u64) = pretty_value.as_u64() { + // // if let Ok(serde_json_value) = + // // serde_json::from_str::(&serde_json_string) + // // { + // // let indentation_string = " ".repeat(pretty_u64 as usize); + // // let serde_formatter = + // // serde_json::ser::PrettyFormatter::with_indent( + // // indentation_string.as_bytes(), + // // ); + // // let serde_buffer = Vec::new(); + // // let mut serde_serializer = + // // serde_json::Serializer::with_formatter( + // // serde_buffer, + // // serde_formatter, + // // ); + // // let serde_json_object = json!(serde_json_value); + + // // if let Ok(()) = + // // serde_json_object.serialize(&mut serde_serializer) + // // { + // // if let Ok(ser_json_string) = + // // String::from_utf8(serde_serializer.into_inner()) + // // { + // // pretty_format_failed = false; + // // serde_json_string = ser_json_string + // // } + // // } + // // } + // // } + + // // if pretty_format_failed { + // // return Value::error(ShellError::labeled_error( + // // "Pretty formatting failed", + // // "failed", + // // pretty_value.tag(), + // // )); + // // } + // // } + + // Value::String { + // val: serde_json_string, + // span: value_span, + // } + // } + // _ => Value::Error { + // error: ShellError::CantConvert("JSON".into(), value_span), + // }, + // } + // } + // _ => Value::Error { + // error: ShellError::CantConvert("JSON".into(), value_span), + // }, + // } + // }, + // engine_state.ctrlc.clone(), + // ) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToJson {}) + } +} diff --git a/crates/nu-command/src/formats/to/mod.rs b/crates/nu-command/src/formats/to/mod.rs new file mode 100644 index 000000000..13c814dd9 --- /dev/null +++ b/crates/nu-command/src/formats/to/mod.rs @@ -0,0 +1,5 @@ +mod command; +mod json; + +pub use command::To; +pub use json::ToJson; diff --git a/crates/nu-json/src/ser.rs b/crates/nu-json/src/ser.rs index 01ea63e86..172784c3c 100644 --- a/crates/nu-json/src/ser.rs +++ b/crates/nu-json/src/ser.rs @@ -9,7 +9,7 @@ use std::num::FpCategory; use super::error::{Error, ErrorCode, Result}; use serde::ser; -use super::util::ParseNumber; +//use super::util::ParseNumber; use regex::Regex; @@ -730,11 +730,15 @@ impl<'a> Formatter for HjsonFormatter<'a> { writer.write_all(&[ch]).map_err(From::from) } - fn comma(&mut self, writer: &mut W, _: bool) -> Result<()> + fn comma(&mut self, writer: &mut W, first: bool) -> Result<()> where W: io::Write, { - writer.write_all(b"\n")?; + if !first { + writer.write_all(b",\n")?; + } else { + writer.write_all(b"\n")?; + } indent(writer, self.current_indent, self.indent) } @@ -848,10 +852,11 @@ where // Check if we can insert this string without quotes // see hjson syntax (must not parse as true, false, null or number) - let mut pn = ParseNumber::new(value.bytes()); - let is_number = pn.parse(true).is_ok(); + //let mut pn = ParseNumber::new(value.bytes()); + //let is_number = pn.parse(true).is_ok(); - if is_number || NEEDS_QUOTES.is_match(value) || STARTS_WITH_KEYWORD.is_match(value) { + if true { + // is_number || NEEDS_QUOTES.is_match(value) || STARTS_WITH_KEYWORD.is_match(value) { // First check if the string can be expressed in multiline format or // we must replace the offending characters with safe escape sequences. @@ -913,11 +918,11 @@ where } // Check if we can insert this name without quotes - if NEEDS_ESCAPE_NAME.is_match(value) { - escape_bytes(wr, value.as_bytes()).map_err(From::from) - } else { - wr.write_all(value.as_bytes()).map_err(From::from) - } + //if NEEDS_ESCAPE_NAME.is_match(value) { + escape_bytes(wr, value.as_bytes()).map_err(From::from) + // } else { + // wr.write_all(value.as_bytes()).map_err(From::from) + // } } #[inline]