googletest_json_serde/matchers/
value_matcher.rs

1//! Utility matchers and macros for concise JSON assertions using googletest.
2
3/// Matches a JSON value (string, number, or boolean) against the given matcher.
4///
5/// This macro enables matching specific primitive values inside a JSON structure
6/// by delegating to a matcher for the corresponding Rust type. It supports:
7/// - `String` values (e.g. `json::value!(eq("hello"))`)
8/// - `Number` values as `i64` or `f64` (e.g. `json::value!(ge(0))`)
9/// - `Boolean` values (e.g. `json::value!(eq(true))`)
10///
11/// Fails if the value is not of the expected JSON type.
12///
13/// # Example
14/// ```
15/// # use googletest::prelude::*;
16/// # use googletest_json_serde::json;
17/// # use serde_json::json as j;
18/// let data = j!({"active": true, "count": 3});
19///
20/// verify_that!(data["active"], json::value!(eq(true)));
21/// verify_that!(data["count"], json::value!(ge(0)));
22/// ```
23#[deprecated(since = "0.2.0", note = "please use `json::primitive!` instead")]
24#[macro_export]
25#[doc(hidden)]
26macro_rules! __json_value {
27    ($matcher:expr) => {
28        $crate::__json_primitive!($matcher)
29    };
30}
31
32/// Matches a JSON value (string, number, or boolean) against the given matcher.
33///
34/// This macro enables matching specific primitive values inside a JSON structure
35/// by delegating to a matcher for the corresponding Rust type. It supports:
36/// - `String` values (e.g. `json::primitive!(eq("hello"))`)
37/// - `Number` values as `i64` or `f64` (e.g. `json::primitive!(ge(0))`)
38/// - `Boolean` values (e.g. `json::primitive!(eq(true))`)
39///
40/// Fails if the value is not of the expected JSON type.
41///
42/// # Example
43/// ```
44/// # use googletest::prelude::*;
45/// # use googletest_json_serde::json;
46/// # use serde_json::json as j;
47/// let data = j!({"active": true, "count": 3});
48///
49/// verify_that!(data["active"], json::primitive!(eq(true)));
50/// verify_that!(data["count"], json::primitive!(ge(0)));
51/// ```
52#[macro_export]
53#[doc(hidden)]
54macro_rules! __json_primitive {
55    ($matcher:expr) => {
56        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonValueMatcher::new(
57            $matcher,
58        )
59    };
60}
61
62pub fn is_null() -> crate::matchers::__internal_unstable_do_not_depend_on_these::IsJsonNull {
63    crate::matchers::__internal_unstable_do_not_depend_on_these::IsJsonNull
64}
65
66pub fn any_value()
67-> crate::matchers::__internal_unstable_do_not_depend_on_these::JsonAnyValueMatcher {
68    crate::matchers::__internal_unstable_do_not_depend_on_these::JsonAnyValueMatcher
69}
70
71#[doc(hidden)]
72pub mod internal {
73    use googletest::description::Description;
74    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
75    use serde_json::Value;
76
77    #[doc(hidden)]
78    #[derive(MatcherBase)]
79    pub struct JsonValueMatcher<M, T> {
80        inner: M,
81        phantom: std::marker::PhantomData<T>,
82    }
83
84    impl<M, T> JsonValueMatcher<M, T> {
85        pub fn new(inner: M) -> Self {
86            Self {
87                inner,
88                phantom: std::marker::PhantomData,
89            }
90        }
91    }
92
93    impl<M> Matcher<&Value> for JsonValueMatcher<M, String>
94    where
95        M: for<'a> Matcher<&'a str>,
96    {
97        fn matches(&self, actual: &Value) -> MatcherResult {
98            match actual {
99                Value::String(s) => self.inner.matches(s),
100                _ => MatcherResult::NoMatch,
101            }
102        }
103        fn describe(&self, r: MatcherResult) -> Description {
104            self.inner.describe(r)
105        }
106        fn explain_match(&self, actual: &Value) -> Description {
107            match actual {
108                Value::String(s) => self.inner.explain_match(s),
109                _ => Description::new().text("which is not a JSON string".to_string()),
110            }
111        }
112    }
113
114    impl<M> Matcher<&Value> for JsonValueMatcher<M, i64>
115    where
116        M: Matcher<i64>,
117    {
118        fn matches(&self, actual: &Value) -> MatcherResult {
119            match actual {
120                Value::Number(n) => n
121                    .as_i64()
122                    .map_or(MatcherResult::NoMatch, |i| self.inner.matches(i)),
123                _ => MatcherResult::NoMatch,
124            }
125        }
126        fn describe(&self, r: MatcherResult) -> Description {
127            self.inner.describe(r)
128        }
129        fn explain_match(&self, actual: &Value) -> Description {
130            match actual {
131                Value::Number(n) => match n.as_i64() {
132                    Some(i) => self.inner.explain_match(i),
133                    None => Description::new().text(format!("number out of i64 range: {n}")),
134                },
135                _ => Description::new().text("which is not a JSON number"),
136            }
137        }
138    }
139
140    impl<M> Matcher<&Value> for JsonValueMatcher<M, f64>
141    where
142        M: Matcher<f64>,
143    {
144        fn matches(&self, actual: &Value) -> MatcherResult {
145            match actual {
146                Value::Number(n) => n
147                    .as_f64()
148                    .map_or(MatcherResult::NoMatch, |f| self.inner.matches(f)),
149                _ => MatcherResult::NoMatch,
150            }
151        }
152        fn describe(&self, r: MatcherResult) -> Description {
153            self.inner.describe(r)
154        }
155        fn explain_match(&self, actual: &Value) -> Description {
156            match actual {
157                Value::Number(n) => match n.as_f64() {
158                    Some(f) => self.inner.explain_match(f),
159                    None => Description::new().text(format!("number not convertible to f64: {n}")),
160                },
161                _ => Description::new().text("which is not a JSON number"),
162            }
163        }
164    }
165
166    impl<M> Matcher<&Value> for JsonValueMatcher<M, bool>
167    where
168        M: Matcher<bool>,
169    {
170        fn matches(&self, actual: &Value) -> MatcherResult {
171            match actual {
172                Value::Bool(b) => self.inner.matches(*b),
173                _ => MatcherResult::NoMatch,
174            }
175        }
176        fn describe(&self, r: MatcherResult) -> Description {
177            self.inner.describe(r)
178        }
179        fn explain_match(&self, actual: &Value) -> Description {
180            match actual {
181                Value::Bool(b) => self.inner.explain_match(*b),
182                _ => Description::new().text("which is not a JSON boolean"),
183            }
184        }
185    }
186
187    #[derive(MatcherBase)]
188    pub struct IsJsonNull;
189    impl Matcher<&Value> for IsJsonNull {
190        fn matches(&self, actual: &Value) -> MatcherResult {
191            match actual {
192                Value::Null => MatcherResult::Match,
193                _ => MatcherResult::NoMatch,
194            }
195        }
196
197        fn describe(&self, _: MatcherResult) -> Description {
198            Description::new().text("JSON null")
199        }
200
201        fn explain_match(&self, actual: &Value) -> Description {
202            match actual {
203                Value::Null => Description::new().text("which is null"),
204                _ => Description::new().text("which is not JSON null"),
205            }
206        }
207    }
208
209    #[derive(MatcherBase)]
210    pub struct JsonAnyValueMatcher;
211    impl JsonMatcher for JsonAnyValueMatcher {}
212    impl Matcher<&Value> for JsonAnyValueMatcher {
213        fn matches(&self, actual: &Value) -> MatcherResult {
214            match actual {
215                Value::Null => MatcherResult::NoMatch,
216                _ => MatcherResult::Match,
217            }
218        }
219
220        fn describe(&self, matcher_result: MatcherResult) -> Description {
221            match matcher_result {
222                MatcherResult::Match => Description::new().text("is any JSON value"),
223                MatcherResult::NoMatch => Description::new().text("never matches"),
224            }
225        }
226
227        fn explain_match(&self, actual: &Value) -> Description {
228            Description::new().text(format!("which is {actual}"))
229        }
230    }
231
232    /// Marker trait for JSON-aware matchers.
233    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {}
234
235    impl<M, T> JsonMatcher for JsonValueMatcher<M, T> where
236        JsonValueMatcher<M, T>: for<'a> Matcher<&'a Value>
237    {
238    }
239
240    impl JsonMatcher for IsJsonNull {}
241
242    /// Trait for converting into a boxed JSON matcher.
243    pub trait IntoJsonMatcher<T> {
244        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>>;
245    }
246
247    impl<J> IntoJsonMatcher<()> for J
248    where
249        J: JsonMatcher + 'static,
250    {
251        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
252            Box::new(self)
253        }
254    }
255
256    impl<M> IntoJsonMatcher<i64> for M
257    where
258        M: Matcher<i64> + 'static,
259    {
260        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
261            Box::new(JsonValueMatcher::<M, i64>::new(self))
262        }
263    }
264
265    impl<M> Matcher<&Value> for JsonValueMatcher<M, u64>
266    where
267        M: Matcher<u64>,
268    {
269        fn matches(&self, actual: &Value) -> MatcherResult {
270            match actual {
271                Value::Number(n) => n
272                    .as_u64()
273                    .map_or(MatcherResult::NoMatch, |u| self.inner.matches(u)),
274                _ => MatcherResult::NoMatch,
275            }
276        }
277        fn describe(&self, r: MatcherResult) -> Description {
278            self.inner.describe(r)
279        }
280        fn explain_match(&self, actual: &Value) -> Description {
281            match actual {
282                Value::Number(n) => match n.as_u64() {
283                    Some(u) => self.inner.explain_match(u),
284                    None => Description::new().text(format!("number out of u64 range: {n}")),
285                },
286                _ => Description::new().text("which is not a JSON number"),
287            }
288        }
289    }
290
291    impl<M> IntoJsonMatcher<u64> for M
292    where
293        M: Matcher<u64> + 'static,
294    {
295        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
296            Box::new(JsonValueMatcher::<M, u64>::new(self))
297        }
298    }
299
300    impl<M> IntoJsonMatcher<f64> for M
301    where
302        M: Matcher<f64> + 'static,
303    {
304        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
305            Box::new(JsonValueMatcher::<M, f64>::new(self))
306        }
307    }
308
309    impl<M> IntoJsonMatcher<String> for M
310    where
311        M: for<'a> Matcher<&'a str> + 'static,
312    {
313        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
314            Box::new(JsonValueMatcher::<M, String>::new(self))
315        }
316    }
317
318    impl<M> IntoJsonMatcher<bool> for M
319    where
320        M: Matcher<bool> + 'static,
321    {
322        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
323            Box::new(JsonValueMatcher::<M, bool>::new(self))
324        }
325    }
326
327    impl<M> Matcher<&Value> for JsonValueMatcher<M, i32>
328    where
329        M: Matcher<i32>,
330    {
331        fn matches(&self, actual: &Value) -> MatcherResult {
332            match actual {
333                Value::Number(n) => match n.as_i64() {
334                    Some(i) => match i32::try_from(i) {
335                        Ok(i32_val) => self.inner.matches(i32_val),
336                        Err(_) => MatcherResult::NoMatch,
337                    },
338                    None => MatcherResult::NoMatch,
339                },
340                _ => MatcherResult::NoMatch,
341            }
342        }
343        fn describe(&self, r: MatcherResult) -> Description {
344            self.inner.describe(r)
345        }
346        fn explain_match(&self, actual: &Value) -> Description {
347            match actual {
348                Value::Number(n) => match n.as_i64() {
349                    Some(i) => match i32::try_from(i) {
350                        Ok(i32_val) => self.inner.explain_match(i32_val),
351                        Err(_) => Description::new().text(format!("number out of i32 range: {n}")),
352                    },
353                    None => Description::new().text(format!("number out of i64 range: {n}")),
354                },
355                _ => Description::new().text("which is not a JSON number"),
356            }
357        }
358    }
359
360    impl<M> IntoJsonMatcher<i32> for M
361    where
362        M: Matcher<i32> + 'static,
363    {
364        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
365            Box::new(JsonValueMatcher::<M, i32>::new(self))
366        }
367    }
368}