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    // Strict version: no `..`
61    ({ $($key:literal : $val:expr),* $(,)? }) => {{
62        let fields = vec![
63            $(
64                ($key,
65                 $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($val)
66                )
67            ),*
68        ];
69        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonObjectMatcher::new ( fields, true )
70    }};
71    // Non-strict version: trailing `..`
72    ({ $($key:literal : $val:expr),* , .. }) => {{
73        let fields = vec![
74            $(
75                ($key,
76                 $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($val)
77                )
78            ),*
79        ];
80        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonObjectMatcher::new ( fields, false )
81    }};
82}
83
84#[doc(hidden)]
85pub mod internal {
86    use crate::matchers::json_matcher::internal::JsonMatcher;
87    use googletest::{
88        description::Description,
89        matcher::{Matcher, MatcherBase, MatcherResult},
90    };
91    use serde_json::{Map, Value};
92
93    type FieldMatcherPair = (&'static str, Box<dyn for<'a> Matcher<&'a Value>>);
94    #[derive(MatcherBase)]
95    pub struct JsonObjectMatcher {
96        fields: Vec<FieldMatcherPair>,
97        strict: bool,
98    }
99
100    impl JsonMatcher for JsonObjectMatcher {}
101
102    impl JsonObjectMatcher {
103        pub fn new(fields: Vec<FieldMatcherPair>, strict: bool) -> Self {
104            Self { fields, strict }
105        }
106
107        fn collect_field_mismatches(&self, obj: &Map<String, Value>) -> Vec<String> {
108            let mut mismatches = Vec::new();
109            for (key, matcher) in &self.fields {
110                match obj.get(*key) {
111                    Some(value) => {
112                        if matcher.matches(value).is_no_match() {
113                            mismatches.push(format!(
114                                "  field '{}': {}",
115                                key,
116                                matcher.explain_match(value)
117                            ));
118                        }
119                    }
120                    None => {
121                        mismatches.push(format!("  field '{key}': was missing"));
122                    }
123                }
124            }
125            mismatches
126        }
127
128        fn collect_unknown_fields(&self, obj: &Map<String, Value>) -> Vec<String> {
129            let mut unknown_fields = Vec::new();
130            for key in obj.keys() {
131                if !self
132                    .fields
133                    .iter()
134                    .any(|(expected_key, _)| expected_key == key)
135                {
136                    unknown_fields.push(format!("  unexpected field '{key}' present"));
137                }
138            }
139            unknown_fields
140        }
141    }
142
143    impl Matcher<&Value> for JsonObjectMatcher {
144        fn matches(&self, actual: &Value) -> MatcherResult {
145            if let Value::Object(obj) = actual {
146                for (k, m) in &self.fields {
147                    match obj.get(*k) {
148                        Some(v) if m.matches(v).is_match() => (),
149                        _ => return MatcherResult::NoMatch,
150                    }
151                }
152                if self.strict && obj.len() != self.fields.len() {
153                    return MatcherResult::NoMatch;
154                }
155                MatcherResult::Match
156            } else {
157                MatcherResult::NoMatch
158            }
159        }
160
161        fn describe(&self, result: MatcherResult) -> Description {
162            if result.is_match() {
163                "has JSON object with expected fields".into()
164            } else {
165                let expected_fields = self
166                    .fields
167                    .iter()
168                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
169                    .collect::<Vec<_>>()
170                    .join("\n");
171                format!("expected JSON object with fields:\n{expected_fields}").into()
172            }
173        }
174        fn explain_match(&self, actual: &Value) -> Description {
175            match actual {
176                Value::Object(obj) => {
177                    let mut mismatches = self.collect_field_mismatches(obj);
178
179                    if self.strict {
180                        let unknown_fields = self.collect_unknown_fields(obj);
181                        mismatches.extend(unknown_fields);
182                    }
183
184                    if mismatches.is_empty() {
185                        Description::new().text("all fields matched as expected")
186                    } else if mismatches.len() == 1 {
187                        Description::new().text(
188                            mismatches
189                                .into_iter()
190                                .next()
191                                .unwrap()
192                                .trim_start()
193                                .to_string(),
194                        )
195                    } else {
196                        Description::new().text(format!(
197                            "had {} field mismatches:\n{}",
198                            mismatches.len(),
199                            mismatches.join("\n")
200                        ))
201                    }
202                }
203                _ => Description::new().text(format!("was {actual} (expected object)")),
204            }
205        }
206    }
207
208    /// Support matching on `Option<Value>` to handle cases where JSON objects may be optional,
209    /// such as API responses that might be null.
210    impl Matcher<&Option<Value>> for JsonObjectMatcher {
211        fn matches(&self, actual: &Option<Value>) -> MatcherResult {
212            match actual {
213                Some(v) => self.matches(v),
214                None => MatcherResult::NoMatch,
215            }
216        }
217
218        fn describe(&self, result: MatcherResult) -> Description {
219            if result.is_match() {
220                "has Some(JSON object) with expected fields".into()
221            } else {
222                let expected_fields = self
223                    .fields
224                    .iter()
225                    .map(|(k, m)| format!("  '{}': {}", k, m.describe(MatcherResult::Match)))
226                    .collect::<Vec<_>>()
227                    .join("\n");
228                format!("expected Some(JSON object) with fields:\n{expected_fields}").into()
229            }
230        }
231
232        fn explain_match(&self, actual: &Option<Value>) -> Description {
233            match actual {
234                Some(value) => {
235                    // Delegate to the main implementation's explain_match
236                    self.explain_match(value)
237                }
238                None => Description::new().text("was None (expected Some(JSON object))"),
239            }
240        }
241    }
242}