Skip to main content

amont_runtime/
json.rs

1//! A minimal JSON emitter for `amont list --json`.
2//!
3//! Twenty lines for a flat, known schema — not a general serialiser. See
4//! `manifest.rs`'s module doc for why this project prefers that trade on the
5//! commit path: this crate must stay dependency-free, and a hand-rolled
6//! escaper for the one shape `CheckListing` needs costs less than a `serde`
7//! tree that runs on every commit in 96 repositories.
8
9/// Escape `s` per the JSON spec: `"`, `\`, and control characters.
10pub fn escape(s: &str) -> String {
11    let mut out = String::with_capacity(s.len() + 2);
12    for c in s.chars() {
13        match c {
14            '"' => out.push_str("\\\""),
15            '\\' => out.push_str("\\\\"),
16            '\n' => out.push_str("\\n"),
17            '\r' => out.push_str("\\r"),
18            '\t' => out.push_str("\\t"),
19            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
20            c => out.push(c),
21        }
22    }
23    out
24}
25
26pub fn string_field(key: &str, value: &str) -> String {
27    format!("\"{}\":\"{}\"", escape(key), escape(value))
28}
29
30pub fn bool_field(key: &str, value: bool) -> String {
31    format!("\"{}\":{value}", escape(key))
32}
33
34/// A number, unquoted — a limit a reader will compare against is worth
35/// emitting as one rather than as a string they have to parse back.
36pub fn int_field(key: &str, value: i64) -> String {
37    format!("\"{}\":{value}", escape(key))
38}
39
40/// `null` when `value` is `None` — used for `stage_filter` and `command`.
41pub fn opt_string_field(key: &str, value: Option<&str>) -> String {
42    match value {
43        Some(v) => string_field(key, v),
44        None => format!("\"{}\":null", escape(key)),
45    }
46}
47
48pub fn string_array_field(key: &str, values: &[String]) -> String {
49    let items: Vec<String> = values
50        .iter()
51        .map(|v| format!("\"{}\"", escape(v)))
52        .collect();
53    format!("\"{}\":[{}]", escape(key), items.join(","))
54}
55
56/// Comma-join already-built `"key":value` fragments into `{...}`.
57pub fn object(fields: &[String]) -> String {
58    format!("{{{}}}", fields.join(","))
59}
60
61/// Comma-join already-built objects into `[...]`.
62pub fn array(items: &[String]) -> String {
63    format!("[{}]", items.join(","))
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn escapes_quote_backslash_and_control_chars() {
72        assert_eq!(escape("a\"b"), "a\\\"b");
73        assert_eq!(escape("a\\b"), "a\\\\b");
74        assert_eq!(escape("a\nb"), "a\\nb");
75        assert_eq!(escape("a\rb"), "a\\rb");
76        assert_eq!(escape("a\tb"), "a\\tb");
77        assert_eq!(escape("a\u{1}b"), "a\\u0001b");
78    }
79
80    #[test]
81    fn plain_text_is_untouched() {
82        assert_eq!(escape("pre-commit-clippy"), "pre-commit-clippy");
83        assert_eq!(escape(""), "");
84    }
85
86    #[test]
87    fn fields_quote_both_key_and_string_value() {
88        assert_eq!(
89            string_field("id", "pre-commit-clippy"),
90            "\"id\":\"pre-commit-clippy\""
91        );
92        assert_eq!(bool_field("pushed", true), "\"pushed\":true");
93        assert_eq!(bool_field("pushed", false), "\"pushed\":false");
94        assert_eq!(opt_string_field("command", None), "\"command\":null");
95        assert_eq!(
96            opt_string_field("command", Some("ruff check")),
97            "\"command\":\"ruff check\""
98        );
99    }
100
101    #[test]
102    fn string_array_field_joins_and_escapes_each_element() {
103        assert_eq!(string_array_field("scope_files", &[]), "\"scope_files\":[]");
104        assert_eq!(
105            string_array_field("scope_files", &[".rs".to_string(), "a\"b".to_string()]),
106            "\"scope_files\":[\".rs\",\"a\\\"b\"]"
107        );
108    }
109
110    #[test]
111    fn object_and_array_join_with_commas() {
112        assert_eq!(
113            object(&["\"a\":1".into(), "\"b\":2".into()]),
114            "{\"a\":1,\"b\":2}"
115        );
116        assert_eq!(array(&["1".into(), "2".into()]), "[1,2]");
117        assert_eq!(object(&[]), "{}");
118        assert_eq!(array(&[]), "[]");
119    }
120}