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 assert_jm!(
152 json!({
153 "a": {
154 "aa": "one",
155 "ab": "two"
156 },
157 "b": "three"
158 }),
159 get_matcher()
160 );
161 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 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 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 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 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 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}