json_matcher/matchers/
object.rs

1use std::collections::{HashMap, HashSet};
2
3use serde_json::Value;
4
5use crate::{JsonMatcher, JsonMatcherError, JsonPath, JsonPathElement};
6
7pub struct ObjectMatcherRefs<'a> {
8    allow_unexpected_keys: bool,
9    fields: HashMap<&'a str, &'a dyn JsonMatcher>,
10}
11
12impl<'a> ObjectMatcherRefs<'a> {
13    pub fn new(allow_unexpected_keys: bool, fields: HashMap<&'a str, &'a dyn JsonMatcher>) -> Self {
14        Self {
15            allow_unexpected_keys,
16            fields,
17        }
18    }
19}
20
21impl JsonMatcher for ObjectMatcherRefs<'_> {
22    fn json_matches(&self, value: &Value) -> Vec<JsonMatcherError> {
23        let mut errors: Vec<JsonMatcherError> = vec![];
24        match value {
25            Value::Object(map) => {
26                let actual_keys = map.keys().map(|x| x.as_str()).collect::<HashSet<&str>>();
27                let expected_keys = self.fields.keys().copied().collect::<HashSet<&str>>();
28                let mut expected_but_missing = expected_keys
29                    .difference(&actual_keys)
30                    .map(|x| x.to_string())
31                    .collect::<Vec<_>>();
32                if !expected_but_missing.is_empty() {
33                    expected_but_missing.sort();
34                    errors.push(JsonMatcherError::at_root(format!(
35                        "Object is missing keys: {}",
36                        expected_but_missing.join(", ")
37                    )));
38                }
39                if !self.allow_unexpected_keys {
40                    let mut unexpected = actual_keys
41                        .difference(&expected_keys)
42                        .map(|x| x.to_string())
43                        .collect::<Vec<_>>();
44                    if !unexpected.is_empty() {
45                        unexpected.sort();
46                        errors.push(JsonMatcherError::at_root(format!(
47                            "Object has unexpected keys: {}",
48                            unexpected.join(", ")
49                        )));
50                    }
51                }
52                let mut expected_and_present = expected_keys
53                    .intersection(&actual_keys).copied()
54                    .collect::<Vec<&str>>();
55                expected_and_present.sort();
56                for key in expected_and_present {
57                    let matcher = self.fields.get(key).expect("Key in fields checked.");
58                    let value = map.get(key).expect("Key in map checked.");
59                    for sub_error in matcher.json_matches(value) {
60                        let this_path = JsonPath::from(vec![
61                            JsonPathElement::Root,
62                            JsonPathElement::Key(key.to_owned()),
63                        ]);
64                        let JsonMatcherError { path, message } = sub_error;
65                        let new_path = this_path.extend(path);
66                        errors.push(JsonMatcherError {
67                            path: new_path,
68                            message,
69                        });
70                    }
71                }
72            }
73            _ => errors.push(JsonMatcherError::at_root("Value is not an object")),
74        }
75        errors
76    }
77}
78
79pub struct ObjectMatcher {
80    allow_unexpected_keys: bool,
81    fields: HashMap<String, Box<dyn JsonMatcher>>,
82}
83
84impl Default for ObjectMatcher {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl ObjectMatcher {
91    pub fn new() -> Self {
92        Self {
93            allow_unexpected_keys: false,
94            fields: HashMap::new(),
95        }
96    }
97
98    pub fn of(fields: HashMap<String, Box<dyn JsonMatcher>>) -> Self {
99        Self {
100            allow_unexpected_keys: false,
101            fields,
102        }
103    }
104
105    pub fn allow_unexpected_keys(mut self) -> Self {
106        self.allow_unexpected_keys = true;
107        self
108    }
109
110    pub fn field(mut self, key: &str, value: impl JsonMatcher + 'static) -> Self {
111        self.fields.insert(key.to_string(), Box::new(value));
112        self
113    }
114}
115
116impl JsonMatcher for ObjectMatcher {
117    fn json_matches(&self, value: &Value) -> Vec<JsonMatcherError> {
118        ObjectMatcherRefs::new(
119            self.allow_unexpected_keys,
120            self.fields
121                .iter()
122                .map(|(k, v)| (k.as_str(), v.as_ref() as &dyn JsonMatcher))
123                .collect(),
124        )
125        .json_matches(value)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use serde_json::json;
132
133    use crate::test::catch_string_panic;
134    use crate::{assert_jm, StringMatcher};
135
136    use super::*;
137
138    #[test]
139    fn test_object_matcher() {
140        let get_matcher = || {
141            ObjectMatcher::new()
142                .field(
143                    "a",
144                    ObjectMatcher::new()
145                        .field("aa", StringMatcher::new("one"))
146                        .field("ab", StringMatcher::new("two")),
147                )
148                .field("b", StringMatcher::new("three"))
149        };
150        // successful match
151        assert_jm!(
152            json!({
153                "a": {
154                    "aa": "one",
155                    "ab": "two"
156                },
157                "b": "three"
158            }),
159            get_matcher()
160        );
161        // problem in a matcher under the root object
162        assert_eq!(
163            catch_string_panic(|| assert_jm!(
164                json!({
165                    "a": {
166                        "aa": "one",
167                        "ab": "two"
168                    },
169                    "b": "four"
170                }),
171                get_matcher()
172            )),
173            r#"
174Json matcher failed:
175  - $.b: Expected string "three" but got "four"
176
177Actual:
178{
179  "a": {
180    "aa": "one",
181    "ab": "two"
182  },
183  "b": "four"
184}"#
185        );
186        // problem in a matcher under a nested object
187        assert_eq!(
188            catch_string_panic(|| assert_jm!(
189                json!({
190                    "a": {
191                        "aa": "one",
192                        "ab": "four"
193                    },
194                    "b": "three"
195                }),
196                get_matcher()
197            )),
198            r#"
199Json matcher failed:
200  - $.a.ab: Expected string "two" but got "four"
201
202Actual:
203{
204  "a": {
205    "aa": "one",
206    "ab": "four"
207  },
208  "b": "three"
209}"#
210        );
211        // unexpected key in root object
212        assert_eq!(
213            catch_string_panic(|| assert_jm!(
214                json!({
215                "a": {
216                    "aa": "one",
217                    "ab": "two"
218                },
219                "b": "three",
220                "c": "four"
221                }),
222                get_matcher()
223            )),
224            r#"
225Json matcher failed:
226  - $: Object has unexpected keys: c
227
228Actual:
229{
230  "a": {
231    "aa": "one",
232    "ab": "two"
233  },
234  "b": "three",
235  "c": "four"
236}"#
237        );
238        // unexpected key in nested object
239        assert_eq!(
240            catch_string_panic(|| assert_jm!(
241                json!({
242                "a": {
243                    "aa": "one",
244                    "ab": "two",
245                    "c": "four"
246                },
247                "b": "three",
248                }),
249                get_matcher()
250            )),
251            r#"
252Json matcher failed:
253  - $.a: Object has unexpected keys: c
254
255Actual:
256{
257  "a": {
258    "aa": "one",
259    "ab": "two",
260    "c": "four"
261  },
262  "b": "three"
263}"#
264        );
265        // multiple issues
266        assert_eq!(
267            catch_string_panic(|| assert_jm!(
268                json!({
269                "a": {
270                    "aa": 2,
271                    "c": "four",
272                },
273                "d": "five",
274                "e": "six"
275                }),
276                get_matcher()
277            )),
278            r#"
279Json matcher failed:
280  - $: Object is missing keys: b
281  - $: Object has unexpected keys: d, e
282  - $.a: Object is missing keys: ab
283  - $.a: Object has unexpected keys: c
284  - $.a.aa: Value is not a string
285
286Actual:
287{
288  "a": {
289    "aa": 2,
290    "c": "four"
291  },
292  "d": "five",
293  "e": "six"
294}"#
295        );
296    }
297
298    #[test]
299    fn test_object_matcher_permissive() {
300        assert_jm!(
301            json!({
302                "a": 1,
303                "b": 2
304            }),
305            ObjectMatcher::new().allow_unexpected_keys().field("a", 1)
306        );
307        // still fails if expected key is missing
308        assert_eq!(
309            catch_string_panic(|| assert_jm!(
310                json!({
311                "b": 2
312                }),
313                ObjectMatcher::new().allow_unexpected_keys().field("a", 1)
314            )),
315            r#"
316Json matcher failed:
317  - $: Object is missing keys: a
318
319Actual:
320{
321  "b": 2
322}"#
323        );
324    }
325}