Skip to main content

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