use clap::Parser; use comfy_table::{ContentArrangement, Row, Table}; use std::fmt; trait DisplayType { fn next_fmt(&self, x: &X) -> Option; fn fmt(&self, x: &X) -> String; fn display(&self, x: &X) -> String { self.next_fmt(x).unwrap_or_else(|| self.fmt(x)) } } trait ToTable { fn to_table(&self) -> Table; } #[derive(Parser)] struct NoDisplay {} impl DisplayType for NoDisplay { fn next_fmt(&self, _: &X) -> Option { None } fn fmt(&self, _: &X) -> String { String::new() } } #[derive(Parser)] struct TextDisplay { /// Display as text #[clap(long, group = "fmt_type")] text: bool, #[clap(flatten)] next: T, } impl DisplayType for TextDisplay where X: fmt::Display, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.text .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { format!("{x}") } } #[derive(Parser)] struct DebugDisplay { /// Display as internal debug representation #[clap(long, group = "fmt_type")] debug: bool, #[clap(flatten)] next: T, } impl DisplayType for DebugDisplay where X: fmt::Debug, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.debug .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { format!("{x:?}") } } #[derive(Parser)] struct ApiDisplay { /// Display as unformatted JSON #[clap(long, group = "fmt_type")] api: bool, #[clap(flatten)] next: T, } impl DisplayType for ApiDisplay where X: serde::Serialize, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.api .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { serde_json::to_string(x).expect("Cannot serialize item to JSON") } } #[derive(Parser)] struct JsonDisplay { /// Display as pretty formatted JSON #[clap(long, group = "fmt_type")] json: bool, #[clap(flatten)] next: T, } impl DisplayType for JsonDisplay where X: serde::Serialize, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.json .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { serde_json::to_string_pretty(x).expect("Cannot serialize item to JSON") } } #[derive(Parser)] struct YamlDisplay { /// Display as YAML #[clap(long, group = "fmt_type")] yaml: bool, #[clap(flatten)] next: T, } impl DisplayType for YamlDisplay where X: serde::Serialize, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.yaml .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { serde_yaml::to_string(x).expect("Cannot serialize item to YAML") } } #[derive(Parser)] struct TableDisplay { /// Display as table #[clap(long, group = "fmt_type", alias = "tabular")] table: bool, #[clap(flatten)] next: T, } impl DisplayType for TableDisplay where X: ToTable, T: DisplayType + clap::Args, { fn next_fmt(&self, x: &X) -> Option { self.table .then(|| self.fmt(x)) .or_else(|| self.next.next_fmt(x)) } fn fmt(&self, x: &X) -> String { x.to_table().to_string() } } #[derive(Parser)] enum App { /// Debug output by default Debug { #[clap(flatten)] output: DebugDisplay>>, }, /// Text output by default Text { #[clap(flatten)] output: TextDisplay>>, }, /// Unformatted JSON output by default Api { #[clap(flatten)] output: ApiDisplay>>, }, /// Pretty formatted JSON output by default Json { #[clap(flatten)] output: JsonDisplay>>>, }, /// YAML output by default Yaml { #[clap(flatten)] output: YamlDisplay>>>>, }, /// Table output by default Table { #[clap(flatten)] output: TableDisplay< YamlDisplay>>>>, >, }, } #[derive(Debug, serde::Serialize)] struct Foo { name: String, value: String, } impl fmt::Display for Foo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}={}", self.name, self.value) } } impl ToTable for Foo { fn to_table(&self) -> Table { let mut table = Table::new(); table .set_content_arrangement(ContentArrangement::Dynamic) .set_header(Row::from(vec!["Name", "Value"])) .add_row(Row::from(vec![&self.name, &self.value])); table } } fn main() { let app = App::parse(); let foo = Foo { name: "Hello".to_string(), value: "world".to_string(), }; match app { App::Debug { output } => println!("{}", output.display(&foo)), App::Text { output } => println!("{}", output.display(&foo)), App::Api { output } => println!("{}", output.display(&foo)), App::Json { output } => println!("{}", output.display(&foo)), App::Yaml { output } => println!("{}", output.display(&foo)), App::Table { output } => println!("{}", output.display(&foo)), } }