Skip to main content

callisto_cli/
output.rs

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
12/// Serializes a `Report` value to JSON, injecting a `"command"` discriminator
13/// field so consumers can distinguish report types (e.g. `"plan-publish"` vs
14/// `"publish"`) without inspecting payload structure.
15pub 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
37/// Trait implemented by CLI report structures to guarantee clean JSON stream splitting and diagnostic card formatting.
38pub 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    /// `log_line` routes purely by `OutputFormat` (Json -> stderr, Text ->
115    /// stdout); it writes directly to the process's real fds rather than a
116    /// caller-supplied writer, so its destination isn't capturable in-process.
117    /// This exercises both branches to prove neither panics.
118    #[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}