aboutsummaryrefslogtreecommitdiff
path: root/src/output.rs
diff options
context:
space:
mode:
authorRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:18:28 +0200
committerRobin Krahl <robin.krahl@ireas.org>2020-09-05 13:18:28 +0200
commit9dca5be8b182773a7b7eb23b47296d403540ead4 (patch)
treedbcd27be5fbdf3ab704a8f9410624e7085234340 /src/output.rs
parent561ee11c37a96ae043c19fcf056576d9892b36a5 (diff)
downloadnitrocli-9dca5be8b182773a7b7eb23b47296d403540ead4.tar.gz
nitrocli-9dca5be8b182773a7b7eb23b47296d403540ead4.tar.bz2
Use dedicated types for output
Previously, we just printed nitrocli’s output to the stdout stream using format macros. To make it possible to support multiple output formats, this patch introduces the output module with dedicated types for the data that is printed to stdout: - Value: A single value, e. g. an OTP code or the configuration data. - Table: A list of objects, rendered as a table, e. g. the list of OTP slots. With this patch, we also drop the --quiet option for the pws get command. It was intended to make it easier to parse the output of the command. But as we will add better output formats for parsing, we no longer need this option. TODO: update changelog
Diffstat (limited to 'src/output.rs')
-rw-r--r--src/output.rs160
1 files changed, 160 insertions, 0 deletions
diff --git a/src/output.rs b/src/output.rs
new file mode 100644
index 0000000..77fb93b
--- /dev/null
+++ b/src/output.rs
@@ -0,0 +1,160 @@
+// output.rs
+
+// Copyright (C) 2020 The Nitrocli Developers
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+//! Defines data types that can be formatted in different output formats.
+
+use std::fmt;
+
+use crate::Context;
+
+/// A trait for objects that can be printed as nitrocli’s output.
+pub trait Output {
+ /// Formats this object as a string that can be printed to the standard output.
+ fn format(&self) -> anyhow::Result<String>;
+
+ /// Prints this object to the output set in the given context.
+ ///
+ /// The default implementation for this method prints the return value of `format` to
+ /// `ctx.stdout`.
+ fn print(&self, ctx: &mut Context<'_>) -> anyhow::Result<()> {
+ println!(ctx, "{}", self.format()?.trim_end()).map_err(From::from)
+ }
+}
+
+/// A single object.
+pub struct Value<T: fmt::Display>(T);
+
+/// A list of objects of the same type that is displayed as a table with a fallback message for an
+/// empty list.
+pub struct Table<T: TableItem> {
+ items: Vec<T>,
+ empty_message: String,
+}
+
+/// A trait for objects that can be displayed in a table.
+pub trait TableItem {
+ /// Returns the column headers for this type of table items.
+ fn headers() -> Vec<&'static str>;
+ /// Returns the values of the column for this table item.
+ fn values(&self) -> Vec<String>;
+}
+
+/// A helper struct for building text reprensetations of objects.
+pub struct TextObject {
+ name: String,
+ items: Vec<(usize, String, String)>,
+}
+
+impl<T: fmt::Display> Value<T> {
+ pub fn new(value: T) -> Value<T> {
+ Value(value)
+ }
+}
+
+impl<T: fmt::Display> Output for Value<T> {
+ fn format(&self) -> anyhow::Result<String> {
+ Ok(self.0.to_string())
+ }
+}
+
+impl<T: TableItem> Table<T> {
+ pub fn new(empty_message: impl Into<String>) -> Table<T> {
+ Table {
+ items: Vec::new(),
+ empty_message: empty_message.into(),
+ }
+ }
+
+ pub fn push(&mut self, item: T) {
+ self.items.push(item);
+ }
+
+ pub fn append(&mut self, vec: &mut Vec<T>) {
+ self.items.append(vec);
+ }
+}
+
+impl<T: TableItem> Output for Table<T> {
+ fn format(&self) -> anyhow::Result<String> {
+ if self.items.is_empty() {
+ Ok(self.empty_message.clone())
+ } else {
+ let headers = T::headers().into_iter().map(ToOwned::to_owned).collect();
+ let values = self.items.iter().map(TableItem::values);
+ Ok(print_table(headers, values))
+ }
+ }
+}
+
+fn print_table<I>(headers: Vec<String>, iter: I) -> String
+where
+ I: Iterator<Item = Vec<String>>,
+{
+ let mut values = Vec::new();
+ values.push(headers);
+ values.extend(iter);
+ let n = values.iter().map(Vec::len).min().unwrap_or_default();
+ let lens: Vec<_> = (0..n)
+ .map(|idx| {
+ values
+ .iter()
+ .map(|v| v[idx].len())
+ .max()
+ .unwrap_or_default()
+ })
+ .collect();
+ values
+ .iter()
+ .map(|v| print_table_line(&lens, &v))
+ .collect::<Vec<_>>()
+ .join("\n")
+}
+
+fn print_table_line(lens: &[usize], values: &[String]) -> String {
+ lens
+ .iter()
+ .zip(values)
+ .map(|(width, value)| format!("{:width$}", value, width = width))
+ .collect::<Vec<_>>()
+ .join("\t")
+}
+
+impl TextObject {
+ pub fn new(name: impl Into<String>) -> TextObject {
+ TextObject {
+ name: name.into(),
+ items: Vec::new(),
+ }
+ }
+
+ pub fn push_line(&mut self, key: impl Into<String>, value: impl Into<String>) {
+ self.items.push((1, key.into(), value.into()));
+ }
+
+ pub fn push_object(&mut self, o: TextObject) {
+ self.push_line(o.name, "");
+ for (indent, key, value) in o.items {
+ self.items.push((1 + indent, key, value));
+ }
+ }
+}
+
+impl fmt::Display for TextObject {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "{}:", self.name)?;
+ let max_len = self
+ .items
+ .iter()
+ .map(|(indent, key, _)| indent * 2 + key.len())
+ .max()
+ .unwrap_or(0);
+ for (indent, key, value) in &self.items {
+ let prefix = " ".repeat(indent * 2);
+ let padding = " ".repeat(max_len - key.len() - indent * 2);
+ writeln!(f, "{}{}:{} {}", prefix, key, padding, value)?;
+ }
+ Ok(())
+ }
+}