googletest_json_serde/matchers/
matches_pattern_matcher.rs

1/// Matches a JSON object by specifying a pattern of key-value matchers, similar to
2/// GoogleTest’s `matches_pattern!` macro for Rust structs.
3///
4/// This macro is used for asserting that a `serde_json::Value` representing a JSON object
5/// contains the specified fields, with each field matching the corresponding matcher. Extra
6/// fields are rejected unless the pattern ends with `..`.
7///
8/// # Examples
9///
10/// Basic usage:
11/// ```
12/// # use googletest::prelude::*;
13/// # use serde_json::json;
14/// # use googletest_json_serde::json;
15/// let value = json!({ "name": "Alice", "age": 30 });
16/// assert_that!(
17///     value,
18///     json::pat!({
19///         "name": eq("Alice"),
20///         "age": ge(29),
21///         .. // allows additional fields
22///     })
23/// );
24/// ```
25///
26/// Nested matching:
27/// ```
28/// # use googletest::prelude::*;
29/// # use serde_json::json;
30/// # use googletest_json_serde::json;
31/// let value = json!({
32///     "user": {
33///         "id": 1,
34///         "active": true
35///     }
36/// });
37/// assert_that!(
38///     value,
39///     json::pat!({
40///         "user": json::pat!({
41///             "id": eq(1),
42///             "active": is_true(),
43///         })
44///     })
45/// );
46/// ```
47///
48/// # Notes
49///
50///  - Both JSON-aware and native GoogleTest matchers (such as `starts_with`, `contains_substring`) can be used directly.
51///  - Wrapping with `json::primitive!` is no longer needed.
52///  - Direct `serde_json::Value` inputs (e.g. `json!(...)`) are supported and compared by structural equality.
53///
54/// # Alias
55///
56/// This macro is reexported as [`json::pat!`](crate::json::pat).
57#[macro_export]
58#[doc(hidden)]
59macro_rules! __json_matches_pattern {
60    (@wrap_matcher $lit:literal) => {
61        $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::<
62            $crate::matchers::__internal_unstable_do_not_depend_on_these::Literal
63        >::into_json_matcher($lit)
64    };
65    (@wrap_matcher $expr:expr) => {
66        $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($expr)
67    };
68    // Strict version: no `..`
69    ({ $($key:literal : $val:expr),* $(,)? }) => {{
70        let fields = vec![
71            $(
72                ($key,
73                 $crate::__json_matches_pattern!(@wrap_matcher $val)
74                )
75            ),*
76        ];
77        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonObjectMatcher::new ( fields, true )
78    }};
79    // Non-strict version: trailing `..`
80    ({ $($key:literal : $val:expr),* , .. }) => {{
81        let fields = vec![
82            $(
83                ($key,
84                 $crate::__json_matches_pattern!(@wrap_matcher $val)
85                )
86            ),*
87        ];
88        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonObjectMatcher::new ( fields, false )
89    }};
90}
91
92#[doc(hidden)]
93pub mod internal {
94    use crate::matchers::json_matcher::internal::JsonMatcher;
95    use googletest::{
96        description::Description,
97        matcher::{Matcher, MatcherBase, MatcherResult},
98    };
99    use serde_json::{Map, Value};
100
101    type FieldMatcherPair = (&'static str, Box<dyn JsonMatcher>);
102    #[derive(MatcherBase)]
103    pub struct JsonObjectMatcher {
104        fields: Vec<FieldMatcherPair>,
105        strict: bool,
106    }
107
108    impl JsonMatcher for JsonObjectMatcher {}
109
110    impl JsonObjectMatcher {
111        pub fn new(fields: Vec<FieldMatcherPair>, strict: bool) -> Self {
112            Self { fields, strict }
113        }
114
115        fn collect_field_mismatches(&self, obj: &Map<String, Value>) -> Vec<String> {
116            let mut mismatches = Vec::new();
117            for (key, matcher) in &self.fields {
118                match obj.get(*key) {
119                    Some(value) => {
120                        if matcher.matches(value).is_no_match() {
121                            mismatches.push(format!(
122                                "  field '{}': {}",
123                                key,
124                                matcher.explain_match(value)
125                            ));
126                        }
127                    }
128                    None => {
129                        if !matcher.allows_missing() {
130                            mismatches.push(format!("  field '{key}': was missing"));
131                        }
132                    }
133                }
134            }
135            mismatches
136        }
137
138        fn collect_unknown_fields(&self, obj: &Map<String, Value>) -> Vec<String> {
139            let mut unknown_fields = Vec::new();
140            for key in obj.keys() {
141                if !self
142                    .fields
143                    .iter()
144                    .any(|(expected_key, _)| expected_key == key)
145                {
146                    unknown_fields.push(format!("  unexpected field '{key}' present"));
147                }
148            }
149            unknown_fields
150        }
151    }
152
153    impl Matcher<&Value> for JsonObjectMatcher {
154        fn matches(&self, actual: &Value) -> MatcherResult {
155            let Value::Object(obj) = actual else {
156                return MatcherResult::NoMatch;
157            };
158
159            // 1. Check all expected fields
160            for (key, matcher) in &self.fields {
161                match obj.get(*key) {
162                    Some(v) => {
163                        if matcher.matches(v).is_no_match() {
164                            return MatcherResult::NoMatch;
165                        }
166                    }
167                    None => {
168                        // Missing field is fine only if the matcher declares it is optional.
169                        if !matcher.allows_missing() {
170                            return MatcherResult::NoMatch;
171                        }
172                    }
173                }
174            }
175
176            // 2. In strict mode, reject unknown fields
177            if self.strict {
178                for actual_key in obj.keys() {
179                    if !self
180                        .fields
181                        .iter()
182                        .any(|(expected_key, _)| expected_key == actual_key)
183                    {
184                        return MatcherResult::NoMatch;
185                    }
186                }
187            }
188
189            MatcherResult::Match
190        }
191
192        fn describe(&self, result: MatcherResult) -> Description {
193            if result.is_match() {
194                "has JSON object with expected fields".into()
195            } else {
196                let expected_fields = self
197                    .fields
198                    .iter()
199                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
200                    .collect::<Vec<_>>()
201                    .join("\n");
202                format!("expected JSON object with fields:\n{expected_fields}").into()
203            }
204        }
205        fn explain_match(&self, actual: &Value) -> Description {
206            match actual {
207                Value::Object(obj) => {
208                    if obj.is_empty() && self.fields.iter().all(|(_, m)| m.allows_missing()) {
209                        Description::new()
210                    } else {
211                        let mut mismatches = self.collect_field_mismatches(obj);
212
213                        if self.strict {
214                            let unknown_fields = self.collect_unknown_fields(obj);
215                            mismatches.extend(unknown_fields);
216                        }
217
218                        if mismatches.is_empty() {
219                            Description::new().text("all fields matched as expected")
220                        } else if mismatches.len() == 1 {
221                            Description::new().text(
222                                mismatches
223                                    .into_iter()
224                                    .next()
225                                    .unwrap()
226                                    .trim_start()
227                                    .to_string(),
228                            )
229                        } else {
230                            Description::new().text(format!(
231                                "had {} field mismatches:\n{}",
232                                mismatches.len(),
233                                mismatches.join("\n")
234                            ))
235                        }
236                    }
237                }
238                _ => Description::new().text(format!("was {actual} (expected object)")),
239            }
240        }
241    }
242
243    /// Support matching on `Option<Value>` to handle cases where JSON objects may be optional,
244    /// such as API responses that might be null.
245    impl Matcher<&Option<Value>> for JsonObjectMatcher {
246        fn matches(&self, actual: &Option<Value>) -> MatcherResult {
247            match actual {
248                Some(v) => self.matches(v),
249                None => MatcherResult::NoMatch,
250            }
251        }
252
253        fn describe(&self, result: MatcherResult) -> Description {
254            if result.is_match() {
255                "has Some(JSON object) with expected fields".into()
256            } else {
257                let expected_fields = self
258                    .fields
259                    .iter()
260                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
261                    .collect::<Vec<_>>()
262                    .join("\n");
263                format!("expected Some(JSON object) with fields:\n{expected_fields}").into()
264            }
265        }
266
267        fn explain_match(&self, actual: &Option<Value>) -> Description {
268            match actual {
269                Some(value) => {
270                    // Delegate to the main implementation's explain_match
271                    self.explain_match(value)
272                }
273                None => Description::new().text("was None (expected Some(JSON object))"),
274            }
275        }
276    }
277}