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