googletest_json_serde/matchers/
matches_pattern_matcher.rs

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