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}