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}