buup/transformers/
json_minifier.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// JSON Minifier transformer
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct JsonMinifier;
6
7impl Transform for JsonMinifier {
8    fn name(&self) -> &'static str {
9        "JSON Minifier"
10    }
11
12    fn id(&self) -> &'static str {
13        "jsonminifier"
14    }
15
16    fn description(&self) -> &'static str {
17        "Minifies a JSON string, removing unnecessary whitespace."
18    }
19
20    fn category(&self) -> TransformerCategory {
21        TransformerCategory::Formatter
22    }
23
24    fn default_test_input(&self) -> &'static str {
25        r#"{
26  "name": "buup",
27  "version": 0.1,
28  "features": [
29    "cli",
30    "web",
31    "lib"
32  ],
33  "active": true,
34  "config": null
35}"#
36    }
37
38    fn transform(&self, input: &str) -> Result<String, TransformError> {
39        // Skip empty input
40        if input.trim().is_empty() {
41            return Ok(String::new());
42        }
43
44        // Replace smart quotes with regular quotes
45        let normalized_input = input.replace(['\u{201C}', '\u{201D}'], "\"");
46
47        minify_json(&normalized_input)
48    }
49}
50
51/// Minify JSON by removing all unnecessary whitespace
52fn minify_json(input: &str) -> Result<String, TransformError> {
53    let mut result = String::with_capacity(input.len());
54    let chars = input.chars();
55    let mut in_string = false;
56    let mut escaped = false;
57
58    for c in chars {
59        if in_string {
60            // Always include characters within strings
61            result.push(c);
62
63            if escaped {
64                // Previous character was escape - this character is always included
65                escaped = false;
66            } else if c == '\\' {
67                escaped = true;
68            } else if c == '"' {
69                in_string = false;
70            }
71        } else {
72            match c {
73                // Start of a string - always include the quote and set flag
74                '"' => {
75                    result.push(c);
76                    in_string = true;
77                }
78                // Structural characters - always include
79                '{' | '}' | '[' | ']' | ':' | ',' => {
80                    result.push(c);
81                }
82                // Whitespace outside a string - skip
83                ' ' | '\t' | '\n' | '\r' => {
84                    // Skip whitespace
85                }
86                // Numbers, booleans, null - include
87                '0'..='9' | '-' | '+' | '.' | 'e' | 'E' | 't' | 'f' | 'n' => {
88                    result.push(c);
89                }
90                // Other characters - could be part of literals (true, false, null)
91                'a'..='z' | 'A'..='Z' => {
92                    result.push(c);
93                }
94                // Invalid characters
95                _ => {
96                    return Err(TransformError::JsonParseError(format!(
97                        "Invalid character: '{}'",
98                        c
99                    )))
100                }
101            }
102        }
103    }
104
105    // Ensure we're not in the middle of a string
106    if in_string {
107        return Err(TransformError::JsonParseError("Unterminated string".into()));
108    }
109
110    Ok(result)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_json_minifier_empty() {
119        let transformer = JsonMinifier;
120        assert_eq!(transformer.transform("").unwrap(), "");
121        assert_eq!(transformer.transform("  ").unwrap(), "");
122    }
123
124    #[test]
125    fn test_json_minifier_simple() {
126        let transformer = JsonMinifier;
127        let input = transformer.default_test_input();
128        let expected = r#"{"name":"buup","version":0.1,"features":["cli","web","lib"],"active":true,"config":null}"#;
129        assert_eq!(transformer.transform(input).unwrap(), expected);
130    }
131
132    #[test]
133    fn test_json_minifier_nested() {
134        let transformer = JsonMinifier;
135        let input = r#"{
136  "person": {
137    "name": "John",
138    "age": 30,
139    "address": {
140      "city": "New York",
141      "zip": "10001"
142    }
143  },
144  "active": true
145}"#;
146        let expected = r#"{"person":{"name":"John","age":30,"address":{"city":"New York","zip":"10001"}},"active":true}"#;
147        assert_eq!(transformer.transform(input).unwrap(), expected);
148    }
149
150    #[test]
151    fn test_json_minifier_array() {
152        let transformer = JsonMinifier;
153        let input = r#"[
154  1,
155  2,
156  3,
157  {
158    "name": "John"
159  }
160]"#;
161        let expected = r#"[1,2,3,{"name":"John"}]"#;
162        assert_eq!(transformer.transform(input).unwrap(), expected);
163    }
164
165    #[test]
166    fn test_json_minifier_preserve_strings() {
167        let transformer = JsonMinifier;
168        let input = r#"{
169  "text": "This has   spaces   and \n newlines \t tabs"
170}"#;
171        let expected = r#"{"text":"This has   spaces   and \n newlines \t tabs"}"#;
172        assert_eq!(transformer.transform(input).unwrap(), expected);
173    }
174
175    #[test]
176    fn test_json_minifier_smart_quotes() {
177        let transformer = JsonMinifier;
178
179        let input = r#"{"test":“value”}"#;
180        let expected = r#"{"test":"value"}"#;
181        assert_eq!(transformer.transform(input).unwrap(), expected);
182    }
183}