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 for<'a> Matcher<&'a Value>>);
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                        mismatches.push(format!("  field '{key}': was missing"));
130                    }
131                }
132            }
133            mismatches
134        }
135
136        fn collect_unknown_fields(&self, obj: &Map<String, Value>) -> Vec<String> {
137            let mut unknown_fields = Vec::new();
138            for key in obj.keys() {
139                if !self
140                    .fields
141                    .iter()
142                    .any(|(expected_key, _)| expected_key == key)
143                {
144                    unknown_fields.push(format!("  unexpected field '{key}' present"));
145                }
146            }
147            unknown_fields
148        }
149    }
150
151    impl Matcher<&Value> for JsonObjectMatcher {
152        fn matches(&self, actual: &Value) -> MatcherResult {
153            if let Value::Object(obj) = actual {
154                for (k, m) in &self.fields {
155                    match obj.get(*k) {
156                        Some(v) if m.matches(v).is_match() => (),
157                        _ => return MatcherResult::NoMatch,
158                    }
159                }
160                if self.strict && obj.len() != self.fields.len() {
161                    return MatcherResult::NoMatch;
162                }
163                MatcherResult::Match
164            } else {
165                MatcherResult::NoMatch
166            }
167        }
168
169        fn describe(&self, result: MatcherResult) -> Description {
170            if result.is_match() {
171                "has JSON object with expected fields".into()
172            } else {
173                let expected_fields = self
174                    .fields
175                    .iter()
176                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
177                    .collect::<Vec<_>>()
178                    .join("\n");
179                format!("expected JSON object with fields:\n{expected_fields}").into()
180            }
181        }
182        fn explain_match(&self, actual: &Value) -> Description {
183            match actual {
184                Value::Object(obj) => {
185                    let mut mismatches = self.collect_field_mismatches(obj);
186
187                    if self.strict {
188                        let unknown_fields = self.collect_unknown_fields(obj);
189                        mismatches.extend(unknown_fields);
190                    }
191
192                    if mismatches.is_empty() {
193                        Description::new().text("all fields matched as expected")
194                    } else if mismatches.len() == 1 {
195                        Description::new().text(
196                            mismatches
197                                .into_iter()
198                                .next()
199                                .unwrap()
200                                .trim_start()
201                                .to_string(),
202                        )
203                    } else {
204                        Description::new().text(format!(
205                            "had {} field mismatches:\n{}",
206                            mismatches.len(),
207                            mismatches.join("\n")
208                        ))
209                    }
210                }
211                _ => Description::new().text(format!("was {actual} (expected object)")),
212            }
213        }
214    }
215
216    /// Support matching on `Option<Value>` to handle cases where JSON objects may be optional,
217    /// such as API responses that might be null.
218    impl Matcher<&Option<Value>> for JsonObjectMatcher {
219        fn matches(&self, actual: &Option<Value>) -> MatcherResult {
220            match actual {
221                Some(v) => self.matches(v),
222                None => MatcherResult::NoMatch,
223            }
224        }
225
226        fn describe(&self, result: MatcherResult) -> Description {
227            if result.is_match() {
228                "has Some(JSON object) with expected fields".into()
229            } else {
230                let expected_fields = self
231                    .fields
232                    .iter()
233                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
234                    .collect::<Vec<_>>()
235                    .join("\n");
236                format!("expected Some(JSON object) with fields:\n{expected_fields}").into()
237            }
238        }
239
240        fn explain_match(&self, actual: &Option<Value>) -> Description {
241            match actual {
242                Some(value) => {
243                    // Delegate to the main implementation's explain_match
244                    self.explain_match(value)
245                }
246                None => Description::new().text("was None (expected Some(JSON object))"),
247            }
248        }
249    }
250}