opcda_bridge_client/
output.rs1use clap::ValueEnum;
11use serde::{Deserialize, Serialize};
12use tabled::{Table, Tabled};
13
14#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum, Deserialize, Serialize)]
16#[serde(rename_all = "lowercase")]
17pub enum OutputFormat {
18 Table,
20 Json,
24}
25
26pub fn render<T: Tabled + Serialize>(rows: Vec<T>, format: OutputFormat) -> anyhow::Result<String> {
28 match format {
29 OutputFormat::Table => Ok(Table::new(rows).to_string()),
30 OutputFormat::Json => Ok(serde_json::to_string_pretty(&rows)?),
31 }
32}
33
34pub fn format_error(err: &anyhow::Error, format: OutputFormat) -> String {
42 match format {
43 OutputFormat::Table => format!("Error: {err:?}"),
44 OutputFormat::Json => {
45 let payload = serde_json::json!({ "error": err.to_string() });
46 serde_json::to_string_pretty(&payload)
47 .unwrap_or_else(|_| format!("{{\"error\": \"{err}\"}}"))
48 }
49 }
50}
51
52pub fn resolve_from_cli(cli: &crate::cli::Cli) -> Option<OutputFormat> {
61 if cli.json {
62 Some(OutputFormat::Json)
63 } else {
64 cli.output
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use proptest::prelude::*;
72
73 #[derive(Tabled, Serialize)]
74 struct Row {
75 name: String,
76 count: u32,
77 }
78
79 #[test]
80 fn test_render_table() {
81 let rows = vec![Row {
82 name: "a".into(),
83 count: 1,
84 }];
85 let out = render(rows, OutputFormat::Table).unwrap();
86 assert!(out.contains("name"));
87 assert!(out.contains("a"));
88 }
89
90 #[test]
91 fn test_render_json_shape_and_keys() {
92 let rows = vec![Row {
93 name: "a".into(),
94 count: 1,
95 }];
96 let out = render(rows, OutputFormat::Json).unwrap();
97 let value: serde_json::Value = serde_json::from_str(&out).unwrap();
98 let arr = value.as_array().unwrap();
99 assert_eq!(arr.len(), 1);
100 assert_eq!(arr[0]["name"], "a");
101 assert_eq!(arr[0]["count"], 1);
102 }
103
104 #[test]
105 fn test_render_json_empty_is_bare_array_not_null() {
106 let rows: Vec<Row> = vec![];
107 let out = render(rows, OutputFormat::Json).unwrap();
108 assert_eq!(out, "[]");
109 }
110
111 #[test]
112 fn test_render_json_is_pretty_printed() {
113 let rows = vec![Row {
114 name: "a".into(),
115 count: 1,
116 }];
117 let out = render(rows, OutputFormat::Json).unwrap();
118 assert!(out.contains('\n'), "expected multi-line pretty JSON");
119 }
120
121 #[test]
122 fn test_format_error_table_matches_debug_chain() {
123 let err = anyhow::anyhow!("boom");
124 let out = format_error(&err, OutputFormat::Table);
125 assert_eq!(out, format!("Error: {err:?}"));
126 }
127
128 #[test]
129 fn test_format_error_json_is_valid_json_with_message() {
130 let err = anyhow::anyhow!("boom");
131 let out = format_error(&err, OutputFormat::Json);
132 let value: serde_json::Value = serde_json::from_str(&out).unwrap();
133 assert_eq!(value["error"], "boom");
134 }
135
136 #[test]
137 fn test_output_format_deserialize_lowercase() {
138 #[derive(Deserialize)]
139 struct Wrapper {
140 output: OutputFormat,
141 }
142 let w: Wrapper = toml::from_str("output = \"json\"").unwrap();
143 assert_eq!(w.output, OutputFormat::Json);
144 let w: Wrapper = toml::from_str("output = \"table\"").unwrap();
145 assert_eq!(w.output, OutputFormat::Table);
146 }
147
148 proptest::proptest! {
149 #[test]
150 fn prop_json_errors_are_parseable(message in any::<String>()) {
151 let err = anyhow::anyhow!(message);
152 let rendered = format_error(&err, OutputFormat::Json);
153 prop_assert!(serde_json::from_str::<serde_json::Value>(&rendered).is_ok());
154 }
155 }
156}