1use std::io;
2
3use serde::Serialize;
4
5use crate::cli::OutputFormat;
6
7pub fn write_json<W: io::Write, S: Serialize + ?Sized>(w: &mut W, val: &S) -> io::Result<()> {
8 let text = serde_json::to_string_pretty(val)?;
9 writeln!(w, "{text}")
10}
11
12pub fn write_report_json<W: io::Write, R: callisto_model::Report>(w: &mut W, val: &R) -> io::Result<()> {
16 #[derive(Serialize)]
17 struct WithCommand<'a, T: Serialize> {
18 command: &'static str,
19 #[serde(flatten)]
20 data: &'a T,
21 }
22 let tagged = WithCommand {
23 command: R::COMMAND,
24 data: val,
25 };
26 let text = serde_json::to_string_pretty(&tagged)?;
27 writeln!(w, "{text}")
28}
29
30pub fn log_line(format: OutputFormat, line: &str) {
31 match format {
32 OutputFormat::Json => eprintln!("{line}"),
33 OutputFormat::Text => println!("{line}"),
34 }
35}
36
37pub trait ReportPresenter: Serialize {
39 fn present_json<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
40 write_json(w, self)
41 }
42
43 fn present_human(&self) -> String;
44}
45
46#[cfg(test)]
47mod tests {
48 use callisto_model::{Diagnostic, Report};
49 use serde::Deserialize;
50
51 use super::*;
52
53 #[derive(Serialize, Deserialize)]
54 struct FakeReport {
55 value: u32,
56 }
57
58 impl callisto_model::Report for FakeReport {
59 const COMMAND: &'static str = "fake-report";
60
61 fn schema_version(&self) -> u32 {
62 1
63 }
64
65 fn diagnostics(&self) -> &[Diagnostic] {
66 &[]
67 }
68 }
69
70 impl ReportPresenter for FakeReport {
71 fn present_human(&self) -> String {
72 format!("value={}", self.value)
73 }
74 }
75
76 #[test]
77 fn write_json_serializes_pretty_with_trailing_newline() {
78 let mut buf = Vec::new();
79 write_json(&mut buf, &FakeReport { value: 42 }).unwrap();
80 let text = String::from_utf8(buf).unwrap();
81 assert!(text.contains("\"value\": 42"), "got:\n{text}");
82 assert!(text.ends_with('\n'));
83 }
84
85 #[test]
86 fn write_report_json_injects_command_discriminator() {
87 let mut buf = Vec::new();
88 write_report_json(&mut buf, &FakeReport { value: 7 }).unwrap();
89 let text = String::from_utf8(buf).unwrap();
90 assert!(text.contains("\"command\": \"fake-report\""), "got:\n{text}");
91 assert!(text.contains("\"value\": 7"), "got:\n{text}");
92 }
93
94 #[test]
95 fn present_json_default_impl_delegates_to_write_json() {
96 let mut buf = Vec::new();
97 FakeReport { value: 1 }.present_json(&mut buf).unwrap();
98 let text = String::from_utf8(buf).unwrap();
99 assert!(text.contains("\"value\": 1"), "got:\n{text}");
100 }
101
102 #[test]
103 fn present_human_renders_the_report_value() {
104 assert_eq!(FakeReport { value: 9 }.present_human(), "value=9");
105 }
106
107 #[test]
108 fn report_trait_accessors_expose_schema_version_and_diagnostics() {
109 let report = FakeReport { value: 1 };
110 assert_eq!(report.schema_version(), 1);
111 assert!(report.diagnostics().is_empty());
112 }
113
114 #[test]
119 fn log_line_does_not_panic_for_either_output_format() {
120 log_line(OutputFormat::Json, "to stderr");
121 log_line(OutputFormat::Text, "to stdout");
122 }
123}