diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 870350852f..a7c0cc4b93 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -152,6 +152,8 @@ pub fn create_default_context() -> EngineState { Table, To, ToJson, + ToUrl, + ToToml, Touch, Use, Update, diff --git a/crates/nu-command/src/formats/to/mod.rs b/crates/nu-command/src/formats/to/mod.rs index 13c814dd99..456297235f 100644 --- a/crates/nu-command/src/formats/to/mod.rs +++ b/crates/nu-command/src/formats/to/mod.rs @@ -1,5 +1,9 @@ mod command; mod json; +mod toml; +mod url; +pub use self::toml::ToToml; pub use command::To; pub use json::ToJson; +pub use url::ToUrl; diff --git a/crates/nu-command/src/formats/to/toml.rs b/crates/nu-command/src/formats/to/toml.rs new file mode 100644 index 0000000000..3f0d6100ba --- /dev/null +++ b/crates/nu-command/src/formats/to/toml.rs @@ -0,0 +1,226 @@ +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, +}; + +#[derive(Clone)] +pub struct ToToml; + +impl Command for ToToml { + fn name(&self) -> &str { + "to toml" + } + + fn signature(&self) -> Signature { + Signature::build("to toml").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into .toml text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an TOML string representing the contents of this table", + example: r#"[[foo bar]; ["1" "2"]] | to toml"#, + result: Some(Value::test_string("bar = \"2\"\nfoo = \"1\"\n")), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + to_toml(input, head) + } +} + +// Helper method to recursively convert nu_protocol::Value -> toml::Value +// This shouldn't be called at the top-level +fn helper(v: &Value) -> Result { + Ok(match &v { + Value::Bool { val, .. } => toml::Value::Boolean(*val), + Value::Int { val, .. } => toml::Value::Integer(*val), + Value::Filesize { val, .. } => toml::Value::Integer(*val), + Value::Duration { val, .. } => toml::Value::String(val.to_string()), + Value::Date { val, .. } => toml::Value::String(val.to_string()), + Value::Range { .. } => toml::Value::String("".to_string()), + Value::Float { val, .. } => toml::Value::Float(*val), + Value::String { val, .. } => toml::Value::String(val.clone()), + Value::Record { cols, vals, .. } => { + let mut m = toml::map::Map::new(); + for (k, v) in cols.iter().zip(vals.iter()) { + m.insert(k.clone(), helper(v)?); + } + toml::Value::Table(m) + } + Value::List { vals, .. } => toml::Value::Array(toml_list(vals)?), + Value::Block { .. } => toml::Value::String("".to_string()), + Value::Nothing { .. } => toml::Value::String("".to_string()), + Value::Error { error } => return Err(error.clone()), + Value::Binary { val, .. } => toml::Value::Array( + val.iter() + .map(|x| toml::Value::Integer(*x as i64)) + .collect(), + ), + Value::CellPath { val, .. } => toml::Value::Array( + val.members + .iter() + .map(|x| match &x { + PathMember::String { val, .. } => Ok(toml::Value::String(val.clone())), + PathMember::Int { val, .. } => Ok(toml::Value::Integer(*val as i64)), + }) + .collect::, ShellError>>()?, + ), + Value::CustomValue { .. } => toml::Value::String("".to_string()), + }) +} + +fn toml_list(input: &[Value]) -> Result, ShellError> { + let mut out = vec![]; + + for value in input { + out.push(helper(value)?); + } + + Ok(out) +} + +fn toml_into_pipeline_data( + toml_value: &toml::Value, + value_type: Type, + span: Span, +) -> Result { + match toml::to_string(&toml_value) { + Ok(serde_toml_string) => Ok(Value::String { + val: serde_toml_string, + span, + } + .into_pipeline_data()), + _ => Ok(Value::Error { + error: ShellError::CantConvert("TOML".into(), value_type.to_string(), span), + } + .into_pipeline_data()), + } +} + +fn value_to_toml_value(v: &Value) -> Result { + match v { + Value::Record { .. } => helper(v), + Value::List { ref vals, span } => match &vals[..] { + [Value::Record { .. }, _end @ ..] => helper(v), + _ => Err(ShellError::UnsupportedInput( + "Expected a table with TOML-compatible structure from pipeline".to_string(), + *span, + )), + }, + Value::String { val, span } => { + // Attempt to de-serialize the String + toml::de::from_str(val).map_err(|_| { + ShellError::UnsupportedInput( + format!("{:?} unable to de-serialize string to TOML", val), + *span, + ) + }) + } + _ => Err(ShellError::UnsupportedInput( + format!("{:?} is not a valid top-level TOML", v.get_type()), + v.span().unwrap_or_else(|_| Span::unknown()), + )), + } +} + +fn to_toml(input: PipelineData, span: Span) -> Result { + let value = input.into_value(span); + + let toml_value = value_to_toml_value(&value)?; + match toml_value { + toml::Value::Array(ref vec) => match vec[..] { + [toml::Value::Table(_)] => toml_into_pipeline_data( + vec.iter().next().expect("this should never trigger"), + value.get_type(), + span, + ), + _ => toml_into_pipeline_data(&toml_value, value.get_type(), span), + }, + _ => toml_into_pipeline_data(&toml_value, value.get_type(), span), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nu_protocol::Spanned; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToToml {}) + } + + #[test] + fn test_value_to_toml_value() { + // + // Positive Tests + // + + let mut m = indexmap::IndexMap::new(); + m.insert("rust".to_owned(), Value::test_string("editor")); + m.insert("is".to_owned(), Value::nothing(Span::unknown())); + m.insert( + "features".to_owned(), + Value::List { + vals: vec![Value::test_string("hello"), Value::test_string("array")], + span: Span::unknown(), + }, + ); + let tv = value_to_toml_value(&Value::from(Spanned { + item: m, + span: Span::unknown(), + })) + .expect("Expected Ok from valid TOML dictionary"); + assert_eq!( + tv.get("features"), + Some(&toml::Value::Array(vec![ + toml::Value::String("hello".to_owned()), + toml::Value::String("array".to_owned()) + ])) + ); + // TOML string + let tv = value_to_toml_value(&Value::test_string( + r#" + title = "TOML Example" + + [owner] + name = "Tom Preston-Werner" + dob = 1979-05-27T07:32:00-08:00 # First class dates + + [dependencies] + rustyline = "4.1.0" + sysinfo = "0.8.4" + chrono = { version = "0.4.6", features = ["serde"] } + "#, + )) + .expect("Expected Ok from valid TOML string"); + assert_eq!( + tv.get("title").unwrap(), + &toml::Value::String("TOML Example".to_owned()) + ); + // + // Negative Tests + // + value_to_toml_value(&Value::test_string("not_valid")) + .expect_err("Expected non-valid toml (String) to cause error!"); + value_to_toml_value(&Value::List { + vals: vec![Value::test_string("1")], + span: Span::unknown(), + }) + .expect_err("Expected non-valid toml (Table) to cause error!"); + } +} diff --git a/crates/nu-command/src/formats/to/url.rs b/crates/nu-command/src/formats/to/url.rs new file mode 100644 index 0000000000..718218b1eb --- /dev/null +++ b/crates/nu-command/src/formats/to/url.rs @@ -0,0 +1,94 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, Value, +}; + +#[derive(Clone)] +pub struct ToUrl; + +impl Command for ToUrl { + fn name(&self) -> &str { + "to url" + } + + fn signature(&self) -> Signature { + Signature::build("to url").category(Category::Formats) + } + + fn usage(&self) -> &str { + "Convert table into url-encoded text" + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Outputs an URL string representing the contents of this table", + example: r#"[[foo bar]; ["1" "2"]] | to url"#, + result: Some(Value::test_string("foo=1&bar=2")), + }] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + to_url(input, head) + } +} + +fn to_url(input: PipelineData, head: Span) -> Result { + let output: Result = input + .into_iter() + .map(move |value| match value { + Value::Record { + ref cols, ref vals, .. + } => { + let mut row_vec = vec![]; + for (k, v) in cols.iter().zip(vals.iter()) { + match v.as_string() { + Ok(s) => { + row_vec.push((k.clone(), s.to_string())); + } + _ => { + return Err(ShellError::UnsupportedInput( + "Expected table with string values".to_string(), + head, + )); + } + } + } + + match serde_urlencoded::to_string(row_vec) { + Ok(s) => Ok(s), + _ => Err(ShellError::CantConvert( + "URL".into(), + value.get_type().to_string(), + head, + )), + } + } + other => Err(ShellError::UnsupportedInput( + "Expected a table from pipeline".to_string(), + other.span().unwrap_or_else(|_| Span::unknown()), + )), + }) + .collect(); + + Ok(Value::string(output?, head).into_pipeline_data()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ToUrl {}) + } +}