aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:19:01 +0200
committerRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:34:49 +0200
commit0f3b0bcb6c196a9b93075a8abee897634c9c892f (patch)
treed3af9f19fdc6afe4db9e4c7920d623e9b1404c18
parentbb809992ad543ea4c0c31897fbf2d130394dd80e (diff)
downloadnitrocli-output-formats.tar.gz
nitrocli-output-formats.tar.bz2
Add tsv output formatoutput-formats
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
-rw-r--r--Cargo.lock50
-rw-r--r--Cargo.toml3
-rw-r--r--src/args.rs1
-rw-r--r--src/output.rs59
4 files changed, 113 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2115b2e..5881f10 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -63,6 +63,24 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -103,6 +121,28 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -203,6 +243,7 @@ version = "0.3.3"
dependencies = [
"anyhow",
"base32",
+ "csv",
"directories",
"envy",
"libc",
@@ -347,6 +388,15 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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<T: fmt::Display + serde::Serialize> Output for Value<T> {
fn format(&self, format: args::OutputFormat) -> anyhow::Result<String> {
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<T: TableItem> Output for Table<T> {
fn format(&self, format: args::OutputFormat) -> anyhow::Result<String> {
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<T: serde::Serialize + ?Sized>(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<T: serde::Serialize>(items: &[T]) -> anyhow::Result<String> {
+ 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<T: serde::Serialize>(value: T) -> anyhow::Result<String> {
+ 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<serde_json::Value> {
+ 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
+}