1use crate::{Transform, TransformError, TransformerCategory};
2
3#[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 if input.trim().is_empty() {
31 return Ok(String::new());
32 }
33
34 let normalized_input = input.replace(['\u{201C}', '\u{201D}'], "\"");
36
37 let tokens = tokenize_json(&normalized_input)?;
39
40 format_json(&tokens)
42 }
43}
44
45#[derive(Debug, PartialEq, Eq)]
47enum JsonToken {
48 OpenBrace, CloseBrace, OpenBracket, CloseBracket, Colon, Comma, String(String),
55 Number(String),
56 Bool(bool),
57 Null,
58 Whitespace,
59}
60
61fn 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 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 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 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 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 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 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 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 ' ' | '\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
227fn format_json(tokens: &[JsonToken]) -> Result<String, TransformError> {
229 let mut result = String::new();
230 let mut indent_level = 0;
231 let indent = " "; 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 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 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(' '); }
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 }
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}