Skip to main content

smooth_json/
lib.rs

1#![allow(unknown_lints)]
2#![deny(missing_docs)]
3#![deny(rustdoc::missing_doc_code_examples)]
4
5//! smooth-json
6//!
7//! `smooth-json` provides a utility to flatten a `serde_json` `Value` into a flat `serde_json` `Object`
8//! # Examples
9//! ```
10//! use smooth_json::Flattener;
11//! ```
12
13use serde_json::Map;
14use serde_json::Value;
15use serde_json::json;
16
17/// Flattener is the main driver when flattening JSON
18/// # Examples
19/// ```
20/// use smooth_json;
21///
22/// let flattener = smooth_json::Flattener { ..Default::default() };
23/// ```
24pub struct Flattener<'a> {
25    /// Alternate separator used between keys when flattening
26    /// # Examples
27    /// ```
28    /// use smooth_json;
29    /// let flattener = smooth_json::Flattener { separator: "_", ..Default::default()};
30    /// ```
31    pub separator: &'a str,
32    /// Opinionated flattening format that places values in an array if the object is nested inside an array
33    /// # Examples
34    /// ```
35    /// use smooth_json;
36    /// let flattener = smooth_json::Flattener { alt_array_flattening: true, ..Default::default()};
37    /// ```
38    pub alt_array_flattening: bool,
39    /// Completely flatten JSON and keep array structure in the key when flattening
40    /// # Examples
41    /// ```
42    /// use smooth_json;
43    /// let flattener = smooth_json::Flattener { preserve_arrays: true, ..Default::default()};
44    /// ```
45    pub preserve_arrays: bool,
46}
47
48impl<'a> Default for Flattener<'a> {
49    fn default() -> Self {
50        Flattener {
51            separator: ".",
52            alt_array_flattening: false,
53            preserve_arrays: false,
54        }
55    }
56}
57
58/// This implementation defines the core usage for the `Flattener` structure.
59/// # Examples
60/// ```
61/// use smooth_json;
62/// use serde_json::json;
63///
64/// let flattener = smooth_json::Flattener::new();
65/// let example = json!({
66///     "a": {
67///         "b": "c"
68///     }
69///  });
70///
71/// let flattened_example = flattener.flatten(&example);
72/// ```
73impl<'a> Flattener<'a> {
74    /// Returns a flattener with the default arguments
75    /// # Examples
76    /// ```
77    /// use smooth_json;
78    ///
79    /// let flattener = smooth_json::Flattener::new();
80    /// ```
81    pub fn new() -> Self {
82        Flattener {
83            ..Default::default()
84        }
85    }
86
87    /// Builds a composite key by combining a prefix and suffix with the configured separator.
88    ///
89    /// # Arguments
90    ///
91    /// * `prefix` - The prefix part of the key (can be empty string for root level)
92    /// * `suffix` - The suffix part of the key
93    ///
94    /// # Returns
95    ///
96    /// A formatted key string with the separator inserted between prefix and suffix.
97    fn build_key(&self, prefix: &str, suffix: &str) -> String {
98        let mut key = String::with_capacity(prefix.len() + self.separator.len() + suffix.len());
99        key.push_str(prefix);
100        key.push_str(self.separator);
101        key.push_str(suffix);
102        key
103    }
104
105    /// Flattens JSON variants into a JSON object
106    ///
107    /// # Arguments
108    ///
109    /// * `json` - A serde_json Value to flatten
110    ///
111    /// # Examples
112    /// ```
113    /// use smooth_json;
114    /// use serde_json::json;
115    ///
116    /// let flattener = smooth_json::Flattener::new();
117    /// let example = json!({
118    ///     "name": "John Doe",
119    ///     "age": 43,
120    ///     "address": {
121    ///         "street": "10 Downing Street",
122    ///         "city": "London"
123    ///     },
124    ///     "phones": [
125    ///         "+44 1234567",
126    ///         "+44 2345678"
127    ///     ]
128    ///  });
129    ///
130    /// let flattened_example = flattener.flatten(&example);
131    /// ```
132    pub fn flatten(&self, json: &Value) -> Value {
133        let mut flattened_val = Map::<String, Value>::new();
134        match json {
135            Value::Array(obj_arr) => self.flatten_array(&mut flattened_val, "", obj_arr),
136            Value::Object(obj_val) => self.flatten_object(&mut flattened_val, None, obj_val, false),
137            _ => self.flatten_value(&mut flattened_val, "", json, false),
138        }
139        Value::Object(flattened_val)
140    }
141
142    fn flatten_object(
143        &self,
144        builder: &mut Map<String, Value>,
145        identifier: Option<&str>,
146        obj: &Map<String, Value>,
147        arr: bool,
148    ) {
149        for (k, v) in obj {
150            let expanded_identifier = match identifier {
151                None => k.clone(),
152                Some(id) => self.build_key(id, k),
153            };
154
155            match v {
156                Value::Object(obj_val) => {
157                    self.flatten_object(builder, Some(expanded_identifier.as_str()), obj_val, arr)
158                }
159                Value::Array(obj_arr) => {
160                    self.flatten_array(builder, expanded_identifier.as_str(), obj_arr)
161                }
162                _ => self.flatten_value(builder, expanded_identifier.as_str(), v, arr),
163            }
164        }
165    }
166
167    fn flatten_array(&self, builder: &mut Map<String, Value>, identifier: &str, obj: &[Value]) {
168        // Empty arrays should be preserved, instead of being omitted
169        if obj.is_empty() {
170            builder.insert(identifier.to_string(), Value::Array(vec![]));
171            return;
172        }
173
174        use std::fmt::Write;
175        let mut index_buf = String::new();
176
177        for (k, v) in obj.iter().enumerate() {
178            write!(&mut index_buf, "{}", k).unwrap();
179            let with_key = self.build_key(identifier, &index_buf);
180            let current_identifier = if self.preserve_arrays {
181                with_key.as_str()
182            } else {
183                identifier
184            };
185
186            match v {
187                Value::Object(obj_val) => self.flatten_object(
188                    builder,
189                    Some(current_identifier),
190                    obj_val,
191                    self.alt_array_flattening,
192                ),
193                Value::Array(obj_arr) => self.flatten_array(builder, current_identifier, obj_arr),
194                _ => self.flatten_value(builder, current_identifier, v, self.alt_array_flattening),
195            }
196
197            index_buf.clear();
198        }
199    }
200
201    fn flatten_value(
202        &self,
203        builder: &mut Map<String, Value>,
204        identifier: &str,
205        obj: &Value,
206        arr: bool,
207    ) {
208        let key = identifier.to_string();
209
210        match builder.entry(key) {
211            serde_json::map::Entry::Occupied(mut entry) => {
212                let value = entry.get_mut();
213                if let Some(array) = value.as_array_mut() {
214                    array.push(obj.clone());
215                } else {
216                    let existing = value.clone();
217                    *value = json!(vec![existing, obj.clone()]);
218                }
219            }
220            serde_json::map::Entry::Vacant(entry) => {
221                entry.insert(if arr {
222                    json!(vec![obj.clone()])
223                } else {
224                    obj.clone()
225                });
226            }
227        }
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    use serde_json::json;
236
237    #[test]
238    fn serde_example() {
239        let flattener = Flattener::new();
240        let base: Value = json!({
241            "name": "John Doe",
242            "age": 43,
243            "address": {
244                "street": "10 Downing Street",
245                "city": "London"
246            },
247            "phones": [
248                "+44 1234567",
249                "+44 2345678"
250            ]
251        });
252
253        let flat = flattener.flatten(&base);
254
255        assert_eq!(
256            flat,
257            json!({
258                "name": "John Doe",
259                "age": 43,
260                "address.street": "10 Downing Street",
261                "address.city": "London",
262                "phones": [
263                    "+44 1234567",
264                    "+44 2345678"
265                ]
266            })
267        );
268    }
269
270    #[test]
271    fn collision_object() {
272        let flattener = Flattener::new();
273        let base: Value = json!({
274          "a": {
275            "b": "c",
276          },
277          "a.b": "d",
278        });
279        let flat = flattener.flatten(&base);
280
281        assert_eq!(
282            flat,
283            json!({
284                "a.b": ["c", "d"],
285            })
286        );
287    }
288
289    #[test]
290    fn collision_array() {
291        let flattener = Flattener::new();
292        let flattener_alt = Flattener {
293            alt_array_flattening: true,
294            ..Default::default()
295        };
296
297        let base: Value = json!({
298          "a": [
299            { "b": "c" },
300            { "b": "d", "c": "e" },
301            [35],
302          ],
303          "a.b": "f",
304        });
305
306        let flat = flattener.flatten(&base);
307        let flat_alt = flattener_alt.flatten(&base);
308
309        assert_eq!(
310            flat,
311            json!({
312                "a.b": ["c", "d", "f"],
313                "a.c": "e",
314                "a": 35,
315            })
316        );
317
318        assert_eq!(
319            flat_alt,
320            json!({
321                "a.b": ["c", "d", "f"],
322                "a.c": ["e"],
323                "a": [35],
324            })
325        );
326    }
327
328    #[test]
329    fn nested_arrays() {
330        let flattener = Flattener::new();
331        let flattener_alt = Flattener {
332            alt_array_flattening: true,
333            ..Default::default()
334        };
335
336        let base: Value = json!({
337          "a": [
338            ["b", "c"],
339            { "d": "e" },
340            ["f", "g"],
341            [
342                { "h": "i" },
343                { "d": "j" },
344            ],
345            ["k", "l"],
346          ]
347        });
348        let flat = flattener.flatten(&base);
349        let flat_alt = flattener_alt.flatten(&base);
350
351        assert_eq!(
352            flat,
353            json!({
354                "a": ["b", "c", "f", "g", "k", "l"],
355                "a.d": ["e", "j"],
356                "a.h": "i",
357            })
358        );
359
360        assert_eq!(
361            flat_alt,
362            json!({
363                "a": ["b", "c", "f", "g", "k", "l"],
364                "a.d": ["e", "j"],
365                "a.h": ["i"],
366            })
367        );
368    }
369
370    #[test]
371    fn nested_arrays_and_objects() {
372        let flattener = Flattener::new();
373        let flattener_alt = Flattener {
374            alt_array_flattening: true,
375            ..Default::default()
376        };
377
378        let base: Value = json!({
379          "a": [
380            "b",
381            ["c", "d"],
382            { "e": ["f", "g"] },
383            [
384                { "h": "i" },
385                { "e": ["j", { "z": "y" }] },
386            ],
387            ["l"],
388            "m",
389          ]
390        });
391        let flat = flattener.flatten(&base);
392        let flat_alt = flattener_alt.flatten(&base);
393
394        assert_eq!(
395            flat,
396            json!({
397                "a": ["b", "c", "d", "l", "m"],
398                "a.e": ["f", "g", "j"],
399                "a.h": "i",
400                "a.e.z": "y",
401            })
402        );
403
404        assert_eq!(
405            flat_alt,
406            json!({
407                "a": ["b", "c", "d", "l", "m"],
408                "a.e": ["f", "g", "j"],
409                "a.h": ["i"],
410                "a.e.z": ["y"],
411            })
412        )
413    }
414
415    #[test]
416    fn custom_separator() {
417        let flattener = Flattener {
418            separator: "$",
419            ..Default::default()
420        };
421
422        let input: Value = json!({
423        "a": {
424            "b": 1
425        }});
426
427        let result: Value = flattener.flatten(&input);
428        assert_eq!(
429            result,
430            json!({
431                "a$b": 1
432            })
433        );
434    }
435    #[test]
436    fn object() {
437        let flattener = Flattener::new();
438
439        let input: Value = json!({
440            "a": {
441                "b": "1",
442                "c": "2",
443                "d": "3"
444            }
445        });
446
447        let result: Value = flattener.flatten(&input);
448        assert_eq!(
449            result,
450            json!({
451                "a.b": "1",
452                "a.c": "2",
453                "a.d": "3"
454            })
455        );
456    }
457
458    #[test]
459    fn array() {
460        let flattener = Flattener::new();
461
462        let input: Value = json!({
463            "a": [
464                {"b": "1"},
465                {"b": "2"},
466                {"b": "3"},
467            ]
468        });
469
470        let result: Value = flattener.flatten(&input);
471        assert_eq!(
472            result,
473            json!({
474                "a.b": ["1", "2", "3"]
475            })
476        );
477    }
478
479    #[test]
480    fn array_preserve() {
481        let flattener = Flattener {
482            preserve_arrays: true,
483            ..Default::default()
484        };
485
486        let input: Value = json!({
487            "a": [
488                {"b": "1"},
489                {"b": "2"},
490                {"b": "3"},
491            ]
492        });
493
494        let result: Value = flattener.flatten(&input);
495        assert_eq!(
496            result,
497            json!({
498                "a.0.b": "1",
499                "a.1.b": "2",
500                "a.2.b": "3"
501            })
502        );
503    }
504
505    #[test]
506    fn array_no_collision() {
507        let flattener = Flattener::new();
508        let flattener_alt = Flattener {
509            alt_array_flattening: true,
510            ..Default::default()
511        };
512
513        let input: Value = json!({
514            "a": [
515                {"b": ["1"]}
516            ]
517        });
518
519        let flat: Value = flattener.flatten(&input);
520        let flat_alt = flattener_alt.flatten(&input);
521
522        assert_eq!(
523            flat,
524            json!({
525                "a.b": "1"
526            })
527        );
528
529        assert_eq!(
530            flat_alt,
531            json!({
532                "a.b": ["1"]
533            })
534        );
535    }
536
537    // its allowed https://ecma-international.org/publications-and-standards/standards/ecma-404/
538    #[test]
539    fn arr_no_key() {
540        let flattener = Flattener::new();
541
542        let input: Value = json!(["a", "b"]);
543
544        let result: Value = flattener.flatten(&input);
545
546        assert_eq!(result, json!({"": ["a", "b"]}));
547    }
548
549    // its allowed https://ecma-international.org/publications-and-standards/standards/ecma-404/
550    #[test]
551    fn arr_empty_key() {
552        let flattener = Flattener::new();
553
554        let input: Value = json!({
555            "": [
556                "a",
557                "b",
558                {"b": ["1"]}
559            ],
560        });
561        let result: Value = flattener.flatten(&input);
562
563        assert_eq!(result, json!({"": ["a", "b"], ".b": "1"}));
564    }
565
566    #[test]
567    fn only_value() {
568        let flattener = Flattener::new();
569
570        let input: Value = json!("abc");
571        let result: Value = flattener.flatten(&input);
572
573        assert_eq!(result, json!({"": "abc"}));
574    }
575
576    #[test]
577    fn nested_array_preserve() {
578        let flattener = Flattener {
579            preserve_arrays: true,
580            ..Default::default()
581        };
582
583        let input: Value = json!({
584        "a": [
585                    "b",
586                    ["c", "d"],
587                    { "e": ["f", "g"] },
588                    [
589                        { "h": "i" },
590                        { "e": ["j", { "z": "y" }] }
591                    ],
592                    ["l"],
593                    "m"
594                 ]
595        });
596
597        let result: Value = flattener.flatten(&input);
598
599        assert_eq!(
600            result,
601            json!({
602              "a.0": "b",
603              "a.1.0": "c",
604              "a.1.1": "d",
605              "a.2.e.0": "f",
606              "a.2.e.1": "g",
607              "a.3.0.h": "i",
608              "a.3.1.e.0": "j",
609              "a.3.1.e.1.z": "y",
610              "a.4.0": "l",
611              "a.5": "m"
612            })
613        )
614    }
615
616    #[test]
617    fn keys_with_separator() {
618        let flattener = Flattener::new();
619
620        let input: Value = json!({
621            "a.b": "value1",
622            "a": {
623                "b": "value2"
624            }
625        });
626
627        let result = flattener.flatten(&input);
628
629        // Both "a.b" literal key and nested "a.b" should result in collision as an array
630        // Order may vary based on map iteration
631        match &result["a.b"] {
632            Value::Array(arr) => {
633                assert_eq!(arr.len(), 2);
634                assert!(arr.contains(&Value::String("value1".to_string())));
635                assert!(arr.contains(&Value::String("value2".to_string())));
636            }
637            _ => panic!("Expected array for collided keys"),
638        }
639    }
640
641    #[test]
642    fn null_values() {
643        let flattener = Flattener::new();
644
645        let input: Value = json!({
646            "a": null,
647            "b": {
648                "c": null,
649                "d": "value"
650            },
651            "e": [null, "text", null]
652        });
653
654        let result = flattener.flatten(&input);
655
656        assert_eq!(
657            result,
658            json!({
659                "a": null,
660                "b.c": null,
661                "b.d": "value",
662                "e": [null, "text", null]
663            })
664        );
665    }
666
667    #[test]
668    fn collision_stress_test() {
669        let flattener = Flattener::new();
670
671        // Create a structure with many collisions to stress test the Entry API
672        let input: Value = json!({
673            "x": "value1",
674            "data": [
675                { "x": "value2" },
676                { "x": "value3" },
677                { "x": "value4" },
678                { "x": "value5" }
679            ]
680        });
681
682        let result = flattener.flatten(&input);
683
684        // Should have two keys: "x" and "data.x", both should be arrays
685        assert!(result.get("x").is_some());
686        assert!(result.get("data.x").is_some());
687
688        match &result["x"] {
689            Value::String(s) => assert_eq!(s, "value1"),
690            _ => panic!("Expected string for 'x'"),
691        }
692
693        match &result["data.x"] {
694            Value::Array(arr) => {
695                assert_eq!(arr.len(), 4);
696                assert_eq!(arr[0], Value::String("value2".to_string()));
697                assert_eq!(arr[1], Value::String("value3".to_string()));
698                assert_eq!(arr[2], Value::String("value4".to_string()));
699                assert_eq!(arr[3], Value::String("value5".to_string()));
700            }
701            _ => panic!("Expected array for 'data.x'"),
702        }
703    }
704}