Skip to main content

brief/minify/
json.rs

1//! JSON / JSONL minifiers (v0.2). Comment-free formats: the
2//! `MinifyOptions::keep_comments` flag is a no-op here.
3
4use super::{MinifyError, MinifyOutput};
5use serde_json::Value;
6
7/// Parse `source` as a single JSON document and re-serialize compactly.
8/// Whitespace, newlines, and indentation are dropped; field order is
9/// preserved (via `serde_json`'s `preserve_order` feature).
10pub fn minify_json(source: &str) -> Result<MinifyOutput, MinifyError> {
11    let v: Value = serde_json::from_str(source).map_err(|e| MinifyError::new(e.to_string()))?;
12    let s = serde_json::to_string(&v).map_err(|e| MinifyError::new(e.to_string()))?;
13    Ok(MinifyOutput::body(s))
14}
15
16/// Parse `source` as JSONL — one JSON document per line. Empty and
17/// whitespace-only lines are dropped from the output. If any line fails to
18/// parse, the whole minification fails (caller emits the original block
19/// verbatim).
20pub fn minify_jsonl(source: &str) -> Result<MinifyOutput, MinifyError> {
21    let mut out = String::with_capacity(source.len());
22    let mut first = true;
23    for (i, line) in source.lines().enumerate() {
24        if line.trim().is_empty() {
25            continue;
26        }
27        let v: Value = serde_json::from_str(line)
28            .map_err(|e| MinifyError::new(format!("line {}: {}", i + 1, e)))?;
29        let s = serde_json::to_string(&v).map_err(|e| MinifyError::new(e.to_string()))?;
30        if !first {
31            out.push('\n');
32        }
33        out.push_str(&s);
34        first = false;
35    }
36    Ok(MinifyOutput::body(out))
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn json_strips_whitespace() {
45        let out = minify_json(
46            r#"{
47            "a": 1,
48            "b": [1, 2, 3]
49        }"#,
50        )
51        .unwrap();
52        assert_eq!(out.body, r#"{"a":1,"b":[1,2,3]}"#);
53    }
54
55    #[test]
56    fn json_preserves_field_order() {
57        let out = minify_json(r#"{"z":1,"a":2,"m":3}"#).unwrap();
58        assert_eq!(out.body, r#"{"z":1,"a":2,"m":3}"#);
59    }
60
61    #[test]
62    fn json_unicode_string() {
63        let out = minify_json(r#"{ "lang": "日本語" }"#).unwrap();
64        assert_eq!(out.body, r#"{"lang":"日本語"}"#);
65    }
66
67    #[test]
68    fn json_preserves_unicode_escape() {
69        // After a parse-then-serialize round-trip serde_json renders the
70        // character directly, but the codepoint must survive intact.
71        let out = minify_json(r#"{"x":"é"}"#).unwrap();
72        let parsed: Value = serde_json::from_str(&out.body).unwrap();
73        assert_eq!(parsed["x"], serde_json::json!("é"));
74    }
75
76    #[test]
77    fn json_empty_object_and_array() {
78        assert_eq!(minify_json("{}").unwrap().body, "{}");
79        assert_eq!(minify_json("[]").unwrap().body, "[]");
80    }
81
82    #[test]
83    fn json_deeply_nested() {
84        let mut s = String::new();
85        for _ in 0..50 {
86            s.push('[');
87        }
88        s.push('1');
89        for _ in 0..50 {
90            s.push(']');
91        }
92        let out = minify_json(&s).unwrap();
93        assert_eq!(out.body, s);
94    }
95
96    #[test]
97    fn json_large_integer() {
98        // i64::MAX fits losslessly. serde_json parses this as an integer
99        // without precision loss.
100        let out = minify_json("9223372036854775807").unwrap();
101        assert_eq!(out.body, "9223372036854775807");
102    }
103
104    #[test]
105    fn json_invalid_returns_error() {
106        let r = minify_json("{ not valid json }");
107        assert!(r.is_err());
108    }
109
110    #[test]
111    fn jsonl_one_per_line() {
112        let src = "{\"a\":1}\n{\"b\":2}\n{\"c\":3}\n";
113        let out = minify_jsonl(src).unwrap();
114        assert_eq!(out.body, "{\"a\":1}\n{\"b\":2}\n{\"c\":3}");
115    }
116
117    #[test]
118    fn jsonl_drops_blank_lines() {
119        let src = "{\"a\":1}\n\n   \n{\"b\":2}\n";
120        let out = minify_jsonl(src).unwrap();
121        assert_eq!(out.body, "{\"a\":1}\n{\"b\":2}");
122    }
123
124    #[test]
125    fn jsonl_invalid_line_fails() {
126        let src = "{\"a\":1}\nnot json\n{\"b\":2}\n";
127        let r = minify_jsonl(src);
128        assert!(r.is_err());
129        // Error message should reference the offending line number.
130        assert!(r.unwrap_err().message.contains("line 2"));
131    }
132}