From 9dca5be8b182773a7b7eb23b47296d403540ead4 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Sat, 5 Sep 2020 13:18:28 +0200 Subject: Use dedicated types for output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/output.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/output.rs (limited to 'src/output.rs') 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; + + /// 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); + +/// 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 { + items: Vec, + 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; +} + +/// A helper struct for building text reprensetations of objects. +pub struct TextObject { + name: String, + items: Vec<(usize, String, String)>, +} + +impl Value { + pub fn new(value: T) -> Value { + Value(value) + } +} + +impl Output for Value { + fn format(&self) -> anyhow::Result { + Ok(self.0.to_string()) + } +} + +impl Table { + pub fn new(empty_message: impl Into) -> Table { + 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) { + self.items.append(vec); + } +} + +impl Output for Table { + fn format(&self) -> anyhow::Result { + 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(headers: Vec, iter: I) -> String +where + I: Iterator>, +{ + 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::>() + .join("\n") +} + +fn print_table_line(lens: &[usize], values: &[String]) -> String { + lens + .iter() + .zip(values) + .map(|(width, value)| format!("{:width$}", value, width = width)) + .collect::>() + .join("\t") +} + +impl TextObject { + pub fn new(name: impl Into) -> TextObject { + TextObject { + name: name.into(), + items: Vec::new(), + } + } + + pub fn push_line(&mut self, key: impl Into, value: impl Into) { + 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(()) + } +} -- cgit v1.2.1