Skip to main content

keel_cli/
render.rs

1//! Output rendering: the one place JSON is serialized, so every command's
2//! `--json` twin is byte-deterministic the same way.
3//!
4//! Reports are always serialized *through* [`serde_json::Value`], whose map is a
5//! `BTreeMap` (serde_json's default, no `preserve_order`) — so keys sort
6//! alphabetically regardless of struct field order, and an agent can diff two
7//! runs and see only real change (dx-spec §5, "determinism as courtesy").
8
9use std::io::Write;
10
11use serde::Serialize;
12
13use crate::Rendered;
14
15/// Serialize any report to canonical JSON: sorted keys, pretty-printed, one
16/// trailing newline. Panics only on a report that cannot serialize, which is a
17/// programming error (all report structs are plain `Serialize`).
18pub fn to_json(value: &impl Serialize) -> serde_json::Value {
19    serde_json::to_value(value).expect("report value is serializable")
20}
21
22/// Render a [`serde_json::Value`] to the exact bytes the `--json` twin prints.
23pub fn json_string(value: &serde_json::Value) -> String {
24    let mut s = serde_json::to_string_pretty(value).expect("json value serializes");
25    s.push('\n');
26    s
27}
28
29/// Print a [`Rendered`] result to the right stream and return its exit code.
30/// `json` selects the machine twin; otherwise the human prose is printed.
31/// Errors and diagnostics ([`Rendered::to_stderr`]) go to stderr regardless.
32pub fn emit(result: &Rendered, json: bool) -> i32 {
33    let text = if json {
34        json_string(&result.json)
35    } else {
36        let mut t = result.human.clone();
37        if !t.ends_with('\n') {
38            t.push('\n');
39        }
40        t
41    };
42    if result.to_stderr {
43        let _ = std::io::stderr().write_all(text.as_bytes());
44    } else {
45        let _ = std::io::stdout().write_all(text.as_bytes());
46    }
47    result.exit
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use serde_json::json;
54
55    #[test]
56    fn json_keys_sort_alphabetically_regardless_of_input_order() {
57        #[derive(Serialize)]
58        struct Out {
59            zebra: u8,
60            alpha: u8,
61            mango: u8,
62        }
63        let v = to_json(&Out {
64            zebra: 1,
65            alpha: 2,
66            mango: 3,
67        });
68        assert_eq!(
69            json_string(&v),
70            "{\n  \"alpha\": 2,\n  \"mango\": 3,\n  \"zebra\": 1\n}\n"
71        );
72    }
73
74    #[test]
75    fn nested_maps_also_sort() {
76        let v = json!({ "b": { "y": 1, "x": 2 }, "a": 3 });
77        // Round-trip through to_value to normalize (json! preserves order here).
78        let v = to_json(&v);
79        assert_eq!(
80            json_string(&v),
81            "{\n  \"a\": 3,\n  \"b\": {\n    \"x\": 2,\n    \"y\": 1\n  }\n}\n"
82        );
83    }
84}