From 55a2f284d9c74b8d1ab2dbee2dae2ef9d64dbe92 Mon Sep 17 00:00:00 2001 From: Warren Seine Date: Fri, 24 Jul 2020 09:41:22 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20`to=20xml`=20command=20(#2141?= =?UTF-8?q?)=20(#2155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 13 +- Cargo.toml | 1 + crates/nu-cli/Cargo.toml | 1 + crates/nu-cli/src/cli.rs | 1 + crates/nu-cli/src/commands.rs | 2 + crates/nu-cli/src/commands/to_xml.rs | 205 ++++++++++++++++++ crates/nu-cli/tests/format_conversions/mod.rs | 1 + crates/nu-cli/tests/format_conversions/xml.rs | 17 ++ crates/nu-errors/src/lib.rs | 12 + docs/commands/to-xml.md | 76 +++++++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 crates/nu-cli/src/commands/to_xml.rs create mode 100644 crates/nu-cli/tests/format_conversions/xml.rs create mode 100644 docs/commands/to-xml.md diff --git a/Cargo.lock b/Cargo.lock index 5777054612..98d31a1b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,7 +455,7 @@ dependencies = [ "codepage", "encoding_rs", "log", - "quick-xml", + "quick-xml 0.17.2", "serde 1.0.114", "zip", ] @@ -2477,6 +2477,7 @@ dependencies = [ "nu_plugin_to_sqlite", "nu_plugin_tree", "pretty_env_logger", + "quick-xml 0.18.1", "semver 0.10.0", "serde 1.0.114", "starship", @@ -2554,6 +2555,7 @@ dependencies = [ "pretty_env_logger", "ptree", "query_interface", + "quick-xml 0.18.1", "quickcheck", "quickcheck_macros", "rand", @@ -3528,6 +3530,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" +dependencies = [ + "memchr", +] + [[package]] name = "quickcheck" version = "0.9.2" diff --git a/Cargo.toml b/Cargo.toml index ddc8f2a8c8..865a7def3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ futures = {version = "0.3", features = ["compat", "io-compat"]} log = "0.4.8" pretty_env_logger = "0.4.0" starship = "0.43.0" +quick-xml = "0.18.1" [dev-dependencies] nu-test-support = {version = "0.17.0", path = "./crates/nu-test-support"} diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index ab7386fee0..2a170842d2 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -93,6 +93,7 @@ encoding_rs = "0.8.23" rayon = "1.3.1" starship = {version = "0.43.0", optional = true} trash = {version = "1.0.1", optional = true} +quick-xml = "0.18.1" [target.'cfg(unix)'.dependencies] users = "0.10.0" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 00b0c45b74..7cc4abe094 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -384,6 +384,7 @@ pub fn create_default_context( whole_stream_command(ToTSV), whole_stream_command(ToURL), whole_stream_command(ToYAML), + whole_stream_command(ToXML), // File format input whole_stream_command(From), whole_stream_command(FromCSV), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 942df8773d..9d6fb49dd5 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -109,6 +109,7 @@ pub(crate) mod to_md; pub(crate) mod to_toml; pub(crate) mod to_tsv; pub(crate) mod to_url; +pub(crate) mod to_xml; pub(crate) mod to_yaml; pub(crate) mod trim; pub(crate) mod uniq; @@ -238,6 +239,7 @@ pub(crate) use to_md::ToMarkdown; pub(crate) use to_toml::ToTOML; pub(crate) use to_tsv::ToTSV; pub(crate) use to_url::ToURL; +pub(crate) use to_xml::ToXML; pub(crate) use to_yaml::ToYAML; pub(crate) use touch::Touch; pub(crate) use trim::Trim; diff --git a/crates/nu-cli/src/commands/to_xml.rs b/crates/nu-cli/src/commands/to_xml.rs new file mode 100644 index 0000000000..830113297d --- /dev/null +++ b/crates/nu-cli/src/commands/to_xml.rs @@ -0,0 +1,205 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use indexmap::IndexMap; +use nu_errors::ShellError; +use nu_protocol::{Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; +use std::collections::HashSet; +use std::io::Cursor; +use std::io::Write; +use std::iter::FromIterator; + +pub struct ToXML; + +#[derive(Deserialize)] +pub struct ToXMLArgs { + pretty: Option, +} + +#[async_trait] +impl WholeStreamCommand for ToXML { + fn name(&self) -> &str { + "to xml" + } + + fn signature(&self) -> Signature { + Signature::build("to xml").named( + "pretty", + SyntaxShape::Int, + "Formats the XML text with the provided indentation setting", + Some('p'), + ) + } + + fn usage(&self) -> &str { + "Convert table into .xml text" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + to_xml(args, registry).await + } +} + +pub fn add_attributes<'a>( + element: &mut quick_xml::events::BytesStart<'a>, + attributes: &'a IndexMap, +) { + for (k, v) in attributes.iter() { + element.push_attribute((k.as_str(), v.as_str())); + } +} + +pub fn get_attributes(row: &Value) -> Option> { + if let UntaggedValue::Row(r) = &row.value { + if let Some(v) = r.entries.get("attributes") { + if let UntaggedValue::Row(a) = &v.value { + let mut h = IndexMap::new(); + for (k, v) in a.entries.iter() { + h.insert(k.clone(), v.convert_to_string()); + } + return Some(h); + } + } + } + None +} + +pub fn get_children(row: &Value) -> Option<&Vec> { + if let UntaggedValue::Row(r) = &row.value { + if let Some(v) = r.entries.get("children") { + if let UntaggedValue::Table(t) = &v.value { + return Some(t); + } + } + } + None +} + +pub fn is_xml_row(row: &Value) -> bool { + if let UntaggedValue::Row(r) = &row.value { + let keys: HashSet<&String> = HashSet::from_iter(r.keys()); + let children: String = "children".to_string(); + let attributes: String = "attributes".to_string(); + return keys.contains(&children) && keys.contains(&attributes) && keys.len() == 2; + } + false +} + +pub fn write_xml_events( + current: &Value, + writer: &mut quick_xml::Writer, +) -> Result<(), ShellError> { + match ¤t.value { + UntaggedValue::Row(o) => { + for (k, v) in o.entries.iter() { + let mut e = BytesStart::owned(k.as_bytes(), k.len()); + if !is_xml_row(v) { + return Err(ShellError::labeled_error( + "Expected a row with 'children' and 'attributes' columns", + "missing 'children' and 'attributes' columns ", + ¤t.tag, + )); + } + let a = get_attributes(v); + if let Some(ref a) = a { + add_attributes(&mut e, a); + } + writer + .write_event(Event::Start(e)) + .expect("Couldn't open XML node"); + let c = get_children(v); + if let Some(c) = c { + for v in c { + write_xml_events(v, writer)?; + } + } + writer + .write_event(Event::End(BytesEnd::borrowed(k.as_bytes()))) + .expect("Couldn't close XML node"); + } + } + UntaggedValue::Table(t) => { + for v in t { + write_xml_events(v, writer)?; + } + } + _ => { + let s = current.convert_to_string(); + writer + .write_event(Event::Text(BytesText::from_plain_str(s.as_str()))) + .expect("Couldn't write XML text"); + } + } + Ok(()) +} + +async fn to_xml(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + let name_tag = args.call_info.name_tag.clone(); + let name_span = name_tag.span; + let (ToXMLArgs { pretty }, input) = args.process(®istry).await?; + let input: Vec = input.collect().await; + + 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![], + }; + + Ok( + futures::stream::iter(to_process_input.into_iter().map(move |value| { + let mut w = pretty.as_ref().map_or_else( + || quick_xml::Writer::new(Cursor::new(Vec::new())), + |p| { + quick_xml::Writer::new_with_indent( + Cursor::new(Vec::new()), + b' ', + p.value.expect_int() as usize, + ) + }, + ); + + let value_span = value.tag.span; + + match write_xml_events(&value, &mut w) { + Ok(_) => { + let b = w.into_inner().into_inner(); + let s = String::from_utf8(b)?; + ReturnSuccess::value( + UntaggedValue::Primitive(Primitive::String(s)).into_value(&name_tag), + ) + } + Err(_) => Err(ShellError::labeled_error_with_secondary( + "Expected a table with XML-compatible structure from pipeline", + "requires XML-compatible input", + name_span, + "originates from here".to_string(), + value_span, + )), + } + })) + .to_output_stream(), + ) +} + +#[cfg(test)] +mod tests { + use super::ToXML; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(ToXML {}) + } +} diff --git a/crates/nu-cli/tests/format_conversions/mod.rs b/crates/nu-cli/tests/format_conversions/mod.rs index e2be182dba..5af12f9fc1 100644 --- a/crates/nu-cli/tests/format_conversions/mod.rs +++ b/crates/nu-cli/tests/format_conversions/mod.rs @@ -13,4 +13,5 @@ mod tsv; mod url; mod vcf; mod xlsx; +mod xml; mod yaml; diff --git a/crates/nu-cli/tests/format_conversions/xml.rs b/crates/nu-cli/tests/format_conversions/xml.rs new file mode 100644 index 0000000000..b26023af0e --- /dev/null +++ b/crates/nu-cli/tests/format_conversions/xml.rs @@ -0,0 +1,17 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn table_to_xml_text_and_from_xml_text_back_into_table() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + open jonathan.xml + | to xml + | from xml + | get rss.children.channel.children.0.item.children.0.guid.attributes.isPermaLink + | echo $it + "# + )); + + assert_eq!(actual.out, "true"); +} diff --git a/crates/nu-errors/src/lib.rs b/crates/nu-errors/src/lib.rs index 7921809110..09f5564466 100644 --- a/crates/nu-errors/src/lib.rs +++ b/crates/nu-errors/src/lib.rs @@ -833,6 +833,18 @@ impl std::convert::From for ShellError { } } +impl std::convert::From for ShellError { + fn from(input: std::string::FromUtf8Error) -> ShellError { + ShellError::untagged_runtime_error(format!("{}", input)) + } +} + +impl std::convert::From for ShellError { + fn from(input: std::str::Utf8Error) -> ShellError { + ShellError::untagged_runtime_error(format!("{}", input)) + } +} + impl std::convert::From for ShellError { fn from(input: serde_yaml::Error) -> ShellError { ShellError::untagged_runtime_error(format!("{:?}", input)) diff --git a/docs/commands/to-xml.md b/docs/commands/to-xml.md new file mode 100644 index 0000000000..1590cc5e32 --- /dev/null +++ b/docs/commands/to-xml.md @@ -0,0 +1,76 @@ +# to xml + +Converts table data into XML text. + +## Flags + +- `-p`, `--pretty` \: Formats the XML text with the provided indentation setting + +## Example + +```shell +> open jonathan.xml +━━━━━━━━━━━━━━━━ + rss +──────────────── + [table: 1 row] +━━━━━━━━━━━━━━━━ +``` + +```shell +> cat jonathan.xml + + + + Jonathan Turner + http://www.jonathanturner.org + + + + Creating crossplatform Rust terminal apps + <p><img src="/images/pikachu.jpg" alt="Pikachu animation in Windows" /></p> + +<p><em>Look Mom, Pikachu running in Windows CMD!</em></p> + +<p>Part of the adventure is not seeing the way ahead and going anyway.</p> + +Mon, 05 Oct 2015 00:00:00 +0000 +http://www.jonathanturner.org/2015/10/off-to-new-adventures.html +http://www.jonathanturner.org/2015/10/off-to-new-adventures.html + + + + + +``` + +```shell +> open jonathan.xml | to xml --pretty 2 + + + Jonathan Turner + http://www.jonathanturner.org + + + + Creating crossplatform Rust terminal apps + <p><img src="/images/pikachu.jpg" alt="Pikachu animation in Windows" /></p> + +<p><em>Look Mom, Pikachu running in Windows CMD!</em></p> + +<p>Part of the adventure is not seeing the way ahead and going anyway.</p> + +Mon, 05 Oct 2015 00:00:00 +0000 +http://www.jonathanturner.org/2015/10/off-to-new-adventures.html +http://www.jonathanturner.org/2015/10/off-to-new-adventures.html + + + +``` + +Due to XML and internal representation, `to xml` is currently limited, it will: + +- only process table data loaded from XML files (e.g. `open file.json | to xml` will fail) +- drop XML prolog declarations +- drop namespaces +- drop comments