buup/transformers/
json_formatter.rs

1use crate::{Transform, TransformError, TransformerCategory};
2
3/// JSON Formatter transformer
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct JsonFormatter;
6
7impl Transform for JsonFormatter {
8    fn name(&self) -> &'static str {
9        "JSON Formatter"
10    }
11
12    fn id(&self) -> &'static str {
13        "jsonformatter"
14    }
15
16    fn description(&self) -> &'static str {
17        "Formats (pretty-prints) a JSON string."
18    }
19
20    fn category(&self) -> TransformerCategory {
21        TransformerCategory::Formatter
22    }
23
24    fn default_test_input(&self) -> &'static str {
25        r#"{"name":"buup","version":0.1,"features":["cli","web","lib"],"active":true,"config":null}"#
26    }
27
28    fn transform(&self, input: &str) -> Result<String, TransformError> {
29        // Skip empty input
30        if input.trim().is_empty() {
31            return Ok(String::new());
32        }
33
34        // Replace smart quotes with regular quotes
35        let normalized_input = input.replace(['\u{201C}', '\u{201D}'], "\"");
36
37        // First, parse the JSON into tokens
38        let tokens = tokenize_json(&normalized_input)?;
39
40        // Then format the tokens with indentation
41        format_json(&tokens)
42    }
43}
44
45/// Different types of JSON tokens
46#[derive(Debug, PartialEq, Eq)]
47enum JsonToken {
48    OpenBrace,    // {
49    CloseBrace,   // }
50    OpenBracket,  // [
51    CloseBracket, // ]
52    Colon,        // :
53    Comma,        // ,
54    String(String),
55    Number(String),
56    Bool(bool),
57    Null,
58    Whitespace,
59}
60
61/// Tokenize JSON string into tokens
62fn tokenize_json(input: &str) -> Result<Vec<JsonToken>, TransformError> {
63    let mut tokens = Vec::new();
64    let mut chars = input.chars().peekable();
65    let mut pos = 0;
66
67    while let Some(c) = chars.next() {
68        pos += 1;
69
70        match c {
71            '{' => tokens.push(JsonToken::OpenBrace),
72            '}' => tokens.push(JsonToken::CloseBrace),
73            '[' => tokens.push(JsonToken::OpenBracket),
74            ']' => tokens.push(JsonToken::CloseBracket),
75            ':' => tokens.push(JsonToken::Colon),
76            ',' => tokens.push(JsonToken::Comma),
77            '"' => {
78                let mut string = String::new();
79                let mut escaped = false;
80
81                while let Some(ch) = chars.next() {
82                    pos += 1;
83                    if escaped {
84                        // Handle escape sequences
85                        string.push(match ch {
86                            '"' | '\\' | '/' => ch,
87                            'b' => '\u{0008}',
88                            'f' => '\u{000C}',
89                            'n' => '\n',
90                            'r' => '\r',
91                            't' => '\t',
92                            'u' => {
93                                // Unicode escape: \uXXXX
94                                let mut hex = String::new();
95                                for _ in 0..4 {
96                                    if let Some(h) = chars.next() {
97                                        pos += 1;
98                                        hex.push(h);
99                                    } else {
100                                        return Err(TransformError::JsonParseError(
101                                            "Unexpected end of unicode escape sequence".into(),
102                                        ));
103                                    }
104                                }
105
106                                // Parse the hex digits to a char
107                                match u32::from_str_radix(&hex, 16) {
108                                    Ok(n) => match char::from_u32(n) {
109                                        Some(unicode_char) => unicode_char,
110                                        None => {
111                                            return Err(TransformError::JsonParseError(
112                                                "Invalid unicode escape sequence".into(),
113                                            ))
114                                        }
115                                    },
116                                    Err(_) => {
117                                        return Err(TransformError::JsonParseError(
118                                            "Invalid unicode escape sequence".into(),
119                                        ))
120                                    }
121                                }
122                            }
123                            _ => {
124                                return Err(TransformError::JsonParseError(format!(
125                                    "Invalid escape sequence: \\{}",
126                                    ch
127                                )))
128                            }
129                        });
130                        escaped = false;
131                    } else if ch == '\\' {
132                        escaped = true;
133                    } else if ch == '"' {
134                        break;
135                    } else {
136                        string.push(ch);
137                    }
138                }
139
140                tokens.push(JsonToken::String(string));
141            }
142            '-' | '0'..='9' => {
143                let mut number = String::new();
144                number.push(c);
145
146                // Parse the rest of the number
147                while let Some(&ch) = chars.peek() {
148                    if ch.is_ascii_digit()
149                        || ch == '.'
150                        || ch == 'e'
151                        || ch == 'E'
152                        || ch == '+'
153                        || ch == '-'
154                    {
155                        number.push(ch);
156                        chars.next();
157                        pos += 1;
158                    } else {
159                        break;
160                    }
161                }
162
163                tokens.push(JsonToken::Number(number));
164            }
165            't' => {
166                // Parse "true"
167                if chars.next() == Some('r')
168                    && chars.next() == Some('u')
169                    && chars.next() == Some('e')
170                {
171                    pos += 3;
172                    tokens.push(JsonToken::Bool(true));
173                } else {
174                    return Err(TransformError::JsonParseError(format!(
175                        "Invalid token at position {}",
176                        pos
177                    )));
178                }
179            }
180            'f' => {
181                // Parse "false"
182                if chars.next() == Some('a')
183                    && chars.next() == Some('l')
184                    && chars.next() == Some('s')
185                    && chars.next() == Some('e')
186                {
187                    pos += 4;
188                    tokens.push(JsonToken::Bool(false));
189                } else {
190                    return Err(TransformError::JsonParseError(format!(
191                        "Invalid token at position {}",
192                        pos
193                    )));
194                }
195            }
196            'n' => {
197                // Parse "null"
198                if chars.next() == Some('u')
199                    && chars.next() == Some('l')
200                    && chars.next() == Some('l')
201                {
202                    pos += 3;
203                    tokens.push(JsonToken::Null);
204                } else {
205                    return Err(TransformError::JsonParseError(format!(
206                        "Invalid token at position {}",
207                        pos
208                    )));
209                }
210            }
211            // Skip whitespace
212            ' ' | '\t' | '\n' | '\r' => {
213                tokens.push(JsonToken::Whitespace);
214            }
215            _ => {
216                return Err(TransformError::JsonParseError(format!(
217                    "Invalid character at position {}",
218                    pos
219                )))
220            }
221        }
222    }
223
224    Ok(tokens)
225}
226
227/// Format JSON tokens with proper indentation
228fn format_json(tokens: &[JsonToken]) -> Result<String, TransformError> {
229    let mut result = String::new();
230    let mut indent_level = 0;
231    let indent = "  "; // Two spaces per indent level
232    let mut idx = 0;
233    let tokens_len = tokens.len();
234
235    while idx < tokens_len {
236        let token = &tokens[idx];
237
238        match token {
239            JsonToken::OpenBrace | JsonToken::OpenBracket => {
240                result.push(if token == &JsonToken::OpenBrace {
241                    '{'
242                } else {
243                    '['
244                });
245
246                // Check if the next non-whitespace token is a closing bracket
247                let mut peek_idx = idx + 1;
248                let mut empty = false;
249                while peek_idx < tokens_len {
250                    match &tokens[peek_idx] {
251                        JsonToken::Whitespace => peek_idx += 1,
252                        JsonToken::CloseBrace | JsonToken::CloseBracket => {
253                            empty = true;
254                            break;
255                        }
256                        _ => break,
257                    }
258                }
259
260                if !empty {
261                    indent_level += 1;
262                    result.push('\n');
263                    result.push_str(&indent.repeat(indent_level));
264                }
265            }
266            JsonToken::CloseBrace | JsonToken::CloseBracket => {
267                if indent_level > 0 {
268                    // Check if previous non-whitespace token was an opening bracket (empty array/object)
269                    let mut peek_idx = idx - 1;
270                    let mut is_empty = false;
271                    while peek_idx > 0 {
272                        match &tokens[peek_idx] {
273                            JsonToken::Whitespace => peek_idx -= 1,
274                            JsonToken::OpenBrace | JsonToken::OpenBracket => {
275                                is_empty = true;
276                                break;
277                            }
278                            _ => break,
279                        }
280                    }
281
282                    if !is_empty {
283                        indent_level -= 1;
284                        result.push('\n');
285                        result.push_str(&indent.repeat(indent_level));
286                    }
287                }
288                result.push(if token == &JsonToken::CloseBrace {
289                    '}'
290                } else {
291                    ']'
292                });
293            }
294            JsonToken::Colon => {
295                result.push(':');
296                result.push(' '); // Add space after colon
297            }
298            JsonToken::Comma => {
299                result.push(',');
300                result.push('\n');
301                result.push_str(&indent.repeat(indent_level));
302            }
303            JsonToken::String(s) => {
304                result.push('"');
305                result.push_str(s);
306                result.push('"');
307            }
308            JsonToken::Number(n) => {
309                result.push_str(n);
310            }
311            JsonToken::Bool(b) => {
312                result.push_str(if *b { "true" } else { "false" });
313            }
314            JsonToken::Null => {
315                result.push_str("null");
316            }
317            JsonToken::Whitespace => {
318                // Skip whitespace tokens
319            }
320        }
321
322        idx += 1;
323    }
324
325    Ok(result)
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_json_formatter_empty() {
334        let transformer = JsonFormatter;
335        assert_eq!(transformer.transform("").unwrap(), "");
336        assert_eq!(transformer.transform("  ").unwrap(), "");
337    }
338
339    #[test]
340    fn test_json_formatter_simple() {
341        let transformer = JsonFormatter;
342        let input = transformer.default_test_input();
343        let expected = r#"{
344  "name": "buup",
345  "version": 0.1,
346  "features": [
347    "cli",
348    "web",
349    "lib"
350  ],
351  "active": true,
352  "config": null
353}"#;
354        assert_eq!(transformer.transform(input).unwrap(), expected);
355    }
356
357    #[test]
358    fn test_json_formatter_nested() {
359        let transformer = JsonFormatter;
360        let input = r#"{"person":{"name":"John","age":30,"address":{"city":"New York","zip":"10001"}},"active":true}"#;
361        let expected = "{\n  \"person\": {\n    \"name\": \"John\",\n    \"age\": 30,\n    \"address\": {\n      \"city\": \"New York\",\n      \"zip\": \"10001\"\n    }\n  },\n  \"active\": true\n}";
362        assert_eq!(transformer.transform(input).unwrap(), expected);
363    }
364
365    #[test]
366    fn test_json_formatter_array() {
367        let transformer = JsonFormatter;
368        let input = r#"[1,2,3,{"name":"John"}]"#;
369        let expected = "[\n  1,\n  2,\n  3,\n  {\n    \"name\": \"John\"\n  }\n]";
370        assert_eq!(transformer.transform(input).unwrap(), expected);
371    }
372
373    #[test]
374    fn test_json_formatter_empty_objects() {
375        let transformer = JsonFormatter;
376        let input = r#"{"empty":{},"emptyArray":[],"nonempty":{"key":"value"}}"#;
377        let expected = "{\n  \"empty\": {},\n  \"emptyArray\": [],\n  \"nonempty\": {\n    \"key\": \"value\"\n  }\n}";
378        assert_eq!(transformer.transform(input).unwrap(), expected);
379    }
380
381    #[test]
382    fn test_json_formatter_smart_quotes() {
383        let transformer = JsonFormatter;
384        let input = r#"{"name":"buup","message":“Hello world”,"smart_left":"testing","smart_right":"more testing"}"#;
385        let expected = "{\n  \"name\": \"buup\",\n  \"message\": \"Hello world\",\n  \"smart_left\": \"testing\",\n  \"smart_right\": \"more testing\"\n}";
386        assert_eq!(transformer.transform(input).unwrap(), expected);
387    }
388}