json_digest/
json_path.rs

1//! Utility functions to specify subtrees in a JSON document. Path pattern syntax is based on
2//! JQ patterns, see <https://stedolan.github.io/jq/manual/#Basicfilters>
3
4use super::*;
5
6/// Checks if any of the paths exist in the provided value.
7pub fn matches(tree: &serde_json::Value, paths_pattern: &str) -> Result<bool> {
8    for single_alternative in split_alternatives(paths_pattern) {
9        if match_single(tree, single_alternative)? {
10            return Ok(true);
11        }
12    }
13    Ok(false)
14}
15
16/// Checks if a single path exists in the provided value. Assumes no whitespaces and no alternate
17/// paths in parameter.
18pub fn match_single(tree: &serde_json::Value, path: &str) -> Result<bool> {
19    match tree {
20        serde_json::Value::Object(map) => {
21            let (property_name, path_tail_opt) = split_head_tail(path)?;
22            let path_tail = match path_tail_opt {
23                None => return Ok(map.contains_key(property_name)),
24                Some(path_tail) => path_tail,
25            };
26
27            let property_val_opt = map.get(property_name);
28            match property_val_opt {
29                Some(subtree) => match_single(subtree, path_tail),
30                None => Ok(false),
31            }
32        }
33        // TODO should we support arrays?
34        // serde_json::Value::Array(arr) => {???},
35        _ => Ok(false),
36    }
37}
38
39/// Splits a pattern into multiple paths
40///
41/// ```
42/// use json_digest::json_path::split_alternatives;
43/// assert_eq!( split_alternatives(".a , .b.c , .d"), vec![".a", ".b.c", ".d"]);
44/// assert_eq!( split_alternatives(""), Vec::<&str>::new());
45/// ```
46pub fn split_alternatives(paths_pattern: &str) -> Vec<&str> {
47    paths_pattern
48        .split_terminator(',') // split alternative tree paths (enabling trailing comma)
49        .map(|item| item.trim()) // trim all items to enable whitespaces near commas
50        .collect()
51}
52
53/// Splits the first key from the rest of the keys in a path
54///
55/// ```
56/// use json_digest::json_path::split_head_tail;
57/// assert_eq!(split_head_tail(".a").unwrap(), ("a", None));
58/// assert_eq!(split_head_tail(".a.b.c").unwrap(), ("a", Some(".b.c")));
59/// ```
60pub fn split_head_tail(path: &str) -> Result<(&str, Option<&str>)> {
61    if !path.starts_with('.') {
62        bail!("Path must start with '.' but it's: {}", path);
63    }
64    let path = &path[1..];
65
66    let dot_idx_opt = path.find('.');
67    let tuple = match dot_idx_opt {
68        None => (path, None),
69        Some(dot_idx) => {
70            let (path_head, path_tail) = path.split_at(dot_idx);
71            (path_head, Some(path_tail))
72        }
73    };
74    Ok(tuple)
75}
76
77#[cfg(test)]
78mod tests {
79    use serde_json::json;
80
81    use super::*;
82
83    fn sample_json_object() -> serde_json::Value {
84        json!({
85            "name": "John Doe",
86            "age": 43,
87            "phones": [
88                "+42 1234567",
89                "+44 2345678"
90            ],
91            "address": {
92                "country": "Germany",
93                "city": "Berlin",
94                "zip": 1234,
95                "street": {
96                    "name": "Some Street",
97                    "number": "1"
98                }
99            }
100        })
101    }
102
103    #[test]
104    fn path_matches() -> Result<()> {
105        let obj = sample_json_object();
106
107        assert!(matches(&obj, "invalidpath").is_err());
108        assert_eq!(matches(&obj, ".notpresent").ok(), Some(false));
109        assert_eq!(matches(&obj, ".name").ok(), Some(true));
110        assert_eq!(matches(&obj, ".a").ok(), Some(false));
111        assert_eq!(matches(&obj, ".age").ok(), Some(true));
112        assert_eq!(matches(&obj, ".ageover").ok(), Some(false));
113        assert_eq!(matches(&obj, ".phones").ok(), Some(true));
114        assert_eq!(matches(&obj, ".phones.first").ok(), Some(false));
115        assert_eq!(matches(&obj, ".address").ok(), Some(true));
116        assert_eq!(matches(&obj, ".address.country").ok(), Some(true));
117        assert_eq!(matches(&obj, ".address.city").ok(), Some(true));
118        assert_eq!(matches(&obj, ".address.street").ok(), Some(true));
119        assert_eq!(matches(&obj, ".address.street.name").ok(), Some(true));
120        assert_eq!(matches(&obj, ".address.street.number").ok(), Some(true));
121        assert_eq!(matches(&obj, ".address.street.none").ok(), Some(false));
122        assert_eq!(matches(&obj, ".address.phone").ok(), Some(false));
123
124        assert!(matches(&obj, ".none , invalid , .ageover ").is_err());
125        assert_eq!(matches(&obj, ".none , .fake , .ageover ").ok(), Some(false));
126        assert_eq!(matches(&obj, ".none , .fake , .ageover , .age").ok(), Some(true));
127        assert_eq!(matches(&obj, ".addr , .address.street.num, .xxx").ok(), Some(false));
128        assert_eq!(matches(&obj, ".addr , .address.street.number, .xxx").ok(), Some(true));
129        assert_eq!(matches(&obj, ".ad , .address.street.numbers, .xxx").ok(), Some(false));
130
131        Ok(())
132    }
133}