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