Skip to main content

json_traits/
flatten.rs

1use std::collections::BTreeMap;
2
3use serde_json::Value;
4
5use crate::JsonScalar;
6
7/// Flattens a JSON value into dotted paths and leaf values.
8///
9/// This is the Rust form of `PathsAndValuesDictionary`.
10pub trait JsonPaths {
11    /// Walk this JSON value and return every leaf as `path -> scalar`.
12    ///
13    /// Object keys and array indexes are joined with `.`. A scalar root
14    /// (string, number, bool, or null) is stored under the empty path `""`.
15    /// Empty objects and empty arrays contribute no paths, because they have
16    /// no leaves.
17    ///
18    /// # Examples
19    ///
20    /// ```
21    /// use json_traits::{JsonPaths, JsonScalar};
22    /// use serde_json::json;
23    ///
24    /// let document = json!({
25    ///     "prop1": { "prop2": "value" },
26    ///     "contacts": [
27    ///         { "info": { "name": "Stewie" } },
28    ///         { "info": { "number": 12 } },
29    ///         { "info": { "isAwesome": true } }
30    ///     ]
31    /// });
32    ///
33    /// let paths = document.paths_and_values();
34    /// assert_eq!(paths.get("prop1.prop2"), Some(&JsonScalar::from("value")));
35    /// assert_eq!(
36    ///     paths.get("contacts.1.info.number"),
37    ///     Some(&JsonScalar::from_integer(12))
38    /// );
39    /// assert_eq!(
40    ///     paths.get("contacts.2.info.isAwesome"),
41    ///     Some(&JsonScalar::from(true))
42    /// );
43    /// ```
44    #[doc(alias = "PathsAndValuesDictionary")]
45    #[must_use]
46    fn paths_and_values(&self) -> BTreeMap<String, JsonScalar>;
47}
48
49impl JsonPaths for Value {
50    fn paths_and_values(&self) -> BTreeMap<String, JsonScalar> {
51        let mut paths = BTreeMap::new();
52        // One buffer is reused for every path. Before descending, remember
53        // the length and truncate back to it so siblings do not keep the
54        // previous segment.
55        walk(self, &mut String::with_capacity(64), &mut paths);
56        paths
57    }
58}
59
60fn walk(value: &Value, path: &mut String, paths: &mut BTreeMap<String, JsonScalar>) {
61    match value {
62        Value::Object(properties) => {
63            for (key, child) in properties {
64                let length = path.len();
65                push_segment(path, key);
66                walk(child, path, paths);
67                path.truncate(length);
68            }
69        }
70        Value::Array(items) => {
71            for (index, child) in items.iter().enumerate() {
72                let length = path.len();
73                push_index(path, index);
74                walk(child, path, paths);
75                path.truncate(length);
76            }
77        }
78        Value::Null => {
79            paths.insert(path.clone(), JsonScalar::Null);
80        }
81        Value::Bool(value) => {
82            paths.insert(path.clone(), JsonScalar::Bool(*value));
83        }
84        Value::Number(value) => {
85            paths.insert(path.clone(), JsonScalar::Number(value.clone()));
86        }
87        Value::String(value) => {
88            paths.insert(path.clone(), JsonScalar::String(value.clone()));
89        }
90    }
91}
92
93fn push_segment(path: &mut String, segment: &str) {
94    if !path.is_empty() {
95        path.push('.');
96    }
97    path.push_str(segment);
98}
99
100fn push_index(path: &mut String, index: usize) {
101    if !path.is_empty() {
102        path.push('.');
103    }
104    // usize decimal form fits in 20 digits. Writing it here avoids allocating
105    // a temporary string for every array element.
106    let mut digits = [0u8; 20];
107    let mut cursor = digits.len();
108    let mut value = index;
109    loop {
110        cursor -= 1;
111        digits[cursor] = b'0' + (value % 10) as u8;
112        value /= 10;
113        if value == 0 {
114            break;
115        }
116    }
117    path.push_str(std::str::from_utf8(&digits[cursor..]).unwrap());
118}