From 0f3b0bcb6c196a9b93075a8abee897634c9c892f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 5 Sep 2020 13:19:01 +0200 Subject: Add tsv output format This patch adds a new output format, tsv. It uses the tsv crate to generate the data and the serde_json crate to transform structs into key-value pairs. TODO: man page, changelog --- Cargo.lock | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ src/args.rs | 1 + src/output.rs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2115b2e..5881f10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,24 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "bstr" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "cc" version = "1.0.50" @@ -102,6 +120,28 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "csv" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279" +dependencies = [ + "bstr", + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + [[package]] name = "directories" version = "3.0.1" @@ -203,6 +243,7 @@ version = "0.3.3" dependencies = [ "anyhow", "base32", + "csv", "directories", "envy", "libc", @@ -346,6 +387,15 @@ dependencies = [ "thread_local", ] +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", +] + [[package]] name = "regex-syntax" version = "0.6.17" diff --git a/Cargo.toml b/Cargo.toml index a1c0e3b..2658b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,9 @@ version = "1.0" [dependencies.base32] version = "0.4.0" +[dependencies.csv] +version = "1.1.0" + [dependencies.envy] version = "0.4.1" diff --git a/src/args.rs b/src/args.rs index 3936738..f34de81 100644 --- a/src/args.rs +++ b/src/args.rs @@ -63,6 +63,7 @@ Enum! { OutputFormat, [ Json => "json", Text => "text", + Tsv => "tsv", ] } diff --git a/src/output.rs b/src/output.rs index 37daf92..01dee2c 100644 --- a/src/output.rs +++ b/src/output.rs @@ -76,6 +76,7 @@ impl Output for Value { fn format(&self, format: args::OutputFormat) -> anyhow::Result { match format { args::OutputFormat::Json => get_json(&self.key, &self.value), + args::OutputFormat::Tsv => get_tsv_object(&self.value), args::OutputFormat::Text => Ok(self.value.to_string()), } } @@ -103,6 +104,7 @@ impl Output for Table { fn format(&self, format: args::OutputFormat) -> anyhow::Result { match format { args::OutputFormat::Json => get_json(&self.key, &self.items), + args::OutputFormat::Tsv => get_tsv_list(&self.items), args::OutputFormat::Text => { if self.items.is_empty() { Ok(self.empty_message.clone()) @@ -192,3 +194,60 @@ fn get_json(key: &str, value: &T) -> anyhow::Resul let _ = map.insert(key, value); serde_json::to_string_pretty(&map).context("Could not serialize output to JSON") } + +fn get_tsv_list(items: &[T]) -> anyhow::Result { + let mut writer = csv::WriterBuilder::new() + .delimiter(b'\t') + .from_writer(vec![]); + for item in items { + writer + .serialize(item) + .context("Could not serialize output to TSV")?; + } + String::from_utf8(writer.into_inner()?).context("Could not parse TSV output as UTF-8") +} + +fn get_tsv_object(value: T) -> anyhow::Result { + let value = serde_json::to_value(&value).context("Could not serialize output")?; + get_tsv_list(&get_tsv_records(&[], value)) +} + +/// Converts an arbitrary value into a list of TSV records. +/// +/// There are two cases: Scalars are converted to a single value (without headers). Arrays and +/// objects are converted to a list of key-value pairs (with headers). Nested arrays and objects +/// are flattened and their keys are separated by dots. +/// +/// `prefix` is the prefix to use for the keys of arrays and objects, or an empty slice if the +/// given value is the top-level value. +fn get_tsv_records(prefix: &[&str], value: serde_json::Value) -> Vec { + use serde_json::Value; + + let mut vec = Vec::new(); + if (value.is_array() || value.is_object()) && prefix.is_empty() { + vec.push(Value::Array(vec!["key".into(), "value".into()])); + } + + match value { + Value::Object(o) => { + for (key, value) in o { + vec.append(&mut get_tsv_records(&[prefix, &[&key]].concat(), value)); + } + } + Value::Array(a) => { + for (idx, value) in a.into_iter().enumerate() { + let idx = idx.to_string(); + vec.append(&mut get_tsv_records(&[prefix, &[&idx]].concat(), value)); + } + } + _ => { + if prefix.is_empty() { + vec.push(value); + } else { + vec.push(Value::Array(vec![prefix.join(".").into(), value])); + } + } + } + + vec +} -- cgit v1.2.1