googletest_json_serde/matchers/
primitive_matcher.rs

1//! Utility matchers and macros for concise JSON assertions using googletest.
2//!
3//! Examples:
4//! ```
5//! # use googletest::prelude::*;
6//! # use googletest_json_serde::json;
7//! # use serde_json::json as j;
8//! let data = j!({"small": 12i8, "large": 65000u16});
9//! verify_that!(data["small"], json::primitive!(eq(12i8)));
10//! verify_that!(data["large"], json::primitive!(ge(65000u16)));
11//! ```
12
13/// Matches a JSON value (string, number, or boolean) against the given matcher.
14///
15/// This macro enables matching specific primitive values inside a JSON structure
16/// by delegating to a matcher for the corresponding Rust type. It supports:
17/// - `String` values (e.g. `json::value!(eq("hello"))`)
18/// - `Number` values as `i64` or `f64` (e.g. `json::value!(ge(0))`)
19/// - `Boolean` values (e.g. `json::value!(eq(true))`)
20///
21/// Fails if the value is not of the expected JSON type.
22///
23/// # Example
24/// ```
25/// # use googletest::prelude::*;
26/// # use googletest_json_serde::json;
27/// # use serde_json::json as j;
28/// let data = j!({"active": true, "count": 3});
29///
30/// verify_that!(data["active"], json::value!(eq(true)));
31/// verify_that!(data["count"], json::value!(ge(0)));
32/// ```
33#[deprecated(since = "0.2.0", note = "please use `json::primitive!` instead")]
34#[macro_export]
35#[doc(hidden)]
36macro_rules! __json_value {
37    ($matcher:expr) => {
38        $crate::__json_primitive!($matcher)
39    };
40}
41
42/// Matches a JSON value (string, number, or boolean) against the given matcher.
43///
44/// This macro enables matching specific primitive values inside a JSON structure
45/// by delegating to a matcher for the corresponding Rust type. It supports:
46/// - `String` values (e.g. `json::primitive!(eq("hello"))`)
47/// - `Number` values as `i64` or `f64` (e.g. `json::primitive!(ge(0))`)
48/// - `Boolean` values (e.g. `json::primitive!(eq(true))`)
49///
50/// Fails if the value is not of the expected JSON type.
51///
52/// # Example
53/// ```
54/// # use googletest::prelude::*;
55/// # use googletest_json_serde::json;
56/// # use serde_json::json as j;
57/// let data = j!({"active": true, "count": 3, "small": 12i8, "limit": 65000u16});
58///
59/// verify_that!(data["active"], json::primitive!(eq(true)));
60/// verify_that!(data["count"], json::primitive!(ge(0)));
61/// verify_that!(data["small"], json::primitive!(eq(12i8)));
62/// verify_that!(data["limit"], json::primitive!(eq(65000u16)));
63/// ```
64#[macro_export]
65#[doc(hidden)]
66macro_rules! __json_primitive {
67    ($matcher:expr) => {
68        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPrimitiveMatcher::new(
69            $matcher,
70        )
71    };
72}
73
74#[doc(hidden)]
75pub mod internal {
76    use crate::matchers::json_matcher::internal::{IntoJsonMatcher, JsonMatcher};
77    use googletest::description::Description;
78    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
79    use serde_json::Value;
80
81    #[doc(hidden)]
82    #[derive(MatcherBase)]
83    pub struct JsonPrimitiveMatcher<M, T> {
84        inner: M,
85        phantom: std::marker::PhantomData<T>,
86    }
87
88    impl<M, T> JsonPrimitiveMatcher<M, T> {
89        pub fn new(inner: M) -> Self {
90            Self {
91                inner,
92                phantom: std::marker::PhantomData,
93            }
94        }
95    }
96
97    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, String>
98    where
99        M: for<'a> Matcher<&'a str>,
100    {
101        fn matches(&self, actual: &Value) -> MatcherResult {
102            match actual {
103                Value::String(s) => self.inner.matches(s),
104                _ => MatcherResult::NoMatch,
105            }
106        }
107        fn describe(&self, r: MatcherResult) -> Description {
108            self.inner.describe(r)
109        }
110        fn explain_match(&self, actual: &Value) -> Description {
111            match actual {
112                Value::String(s) => self.inner.explain_match(s),
113                _ => Description::new().text("which is not a JSON string".to_string()),
114            }
115        }
116    }
117
118    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i64>
119    where
120        M: Matcher<i64>,
121    {
122        fn matches(&self, actual: &Value) -> MatcherResult {
123            match actual {
124                Value::Number(n) => n
125                    .as_i64()
126                    .map_or(MatcherResult::NoMatch, |i| self.inner.matches(i)),
127                _ => MatcherResult::NoMatch,
128            }
129        }
130        fn describe(&self, r: MatcherResult) -> Description {
131            self.inner.describe(r)
132        }
133        fn explain_match(&self, actual: &Value) -> Description {
134            match actual {
135                Value::Number(n) => match n.as_i64() {
136                    Some(i) => self.inner.explain_match(i),
137                    None => Description::new().text(format!("number out of i64 range: {n}")),
138                },
139                _ => Description::new().text("which is not a JSON number"),
140            }
141        }
142    }
143
144    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, f64>
145    where
146        M: Matcher<f64>,
147    {
148        fn matches(&self, actual: &Value) -> MatcherResult {
149            match actual {
150                Value::Number(n) => n
151                    .as_f64()
152                    .map_or(MatcherResult::NoMatch, |f| self.inner.matches(f)),
153                _ => MatcherResult::NoMatch,
154            }
155        }
156        fn describe(&self, r: MatcherResult) -> Description {
157            self.inner.describe(r)
158        }
159        fn explain_match(&self, actual: &Value) -> Description {
160            match actual {
161                Value::Number(n) => match n.as_f64() {
162                    Some(f) => self.inner.explain_match(f),
163                    None => Description::new().text(format!("number not convertible to f64: {n}")),
164                },
165                _ => Description::new().text("which is not a JSON number"),
166            }
167        }
168    }
169
170    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, bool>
171    where
172        M: Matcher<bool>,
173    {
174        fn matches(&self, actual: &Value) -> MatcherResult {
175            match actual {
176                Value::Bool(b) => self.inner.matches(*b),
177                _ => MatcherResult::NoMatch,
178            }
179        }
180        fn describe(&self, r: MatcherResult) -> Description {
181            self.inner.describe(r)
182        }
183        fn explain_match(&self, actual: &Value) -> Description {
184            match actual {
185                Value::Bool(b) => self.inner.explain_match(*b),
186                _ => Description::new().text("which is not a JSON boolean"),
187            }
188        }
189    }
190
191    impl<M, T> JsonMatcher for JsonPrimitiveMatcher<M, T> where
192        JsonPrimitiveMatcher<M, T>: for<'a> Matcher<&'a Value>
193    {
194    }
195
196    /// Trait for converting into a boxed JSON matcher.
197    impl<M> IntoJsonMatcher<i64> for M
198    where
199        M: Matcher<i64> + 'static,
200    {
201        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
202            Box::new(JsonPrimitiveMatcher::<M, i64>::new(self))
203        }
204    }
205
206    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, u64>
207    where
208        M: Matcher<u64>,
209    {
210        fn matches(&self, actual: &Value) -> MatcherResult {
211            match actual {
212                Value::Number(n) => n
213                    .as_u64()
214                    .map_or(MatcherResult::NoMatch, |u| self.inner.matches(u)),
215                _ => MatcherResult::NoMatch,
216            }
217        }
218        fn describe(&self, r: MatcherResult) -> Description {
219            self.inner.describe(r)
220        }
221        fn explain_match(&self, actual: &Value) -> Description {
222            match actual {
223                Value::Number(n) => match n.as_u64() {
224                    Some(u) => self.inner.explain_match(u),
225                    None => Description::new().text(format!("number out of u64 range: {n}")),
226                },
227                _ => Description::new().text("which is not a JSON number"),
228            }
229        }
230    }
231
232    impl<M> IntoJsonMatcher<u64> for M
233    where
234        M: Matcher<u64> + 'static,
235    {
236        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
237            Box::new(JsonPrimitiveMatcher::<M, u64>::new(self))
238        }
239    }
240
241    impl<M> IntoJsonMatcher<f64> for M
242    where
243        M: Matcher<f64> + 'static,
244    {
245        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
246            Box::new(JsonPrimitiveMatcher::<M, f64>::new(self))
247        }
248    }
249
250    impl<M> IntoJsonMatcher<String> for M
251    where
252        M: for<'a> Matcher<&'a str> + 'static,
253    {
254        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
255            Box::new(JsonPrimitiveMatcher::<M, String>::new(self))
256        }
257    }
258
259    impl<M> IntoJsonMatcher<bool> for M
260    where
261        M: Matcher<bool> + 'static,
262    {
263        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
264            Box::new(JsonPrimitiveMatcher::<M, bool>::new(self))
265        }
266    }
267
268    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i32>
269    where
270        M: Matcher<i32>,
271    {
272        fn matches(&self, actual: &Value) -> MatcherResult {
273            match actual {
274                Value::Number(n) => match n.as_i64() {
275                    Some(i) => match i32::try_from(i) {
276                        Ok(i32_val) => self.inner.matches(i32_val),
277                        Err(_) => MatcherResult::NoMatch,
278                    },
279                    None => MatcherResult::NoMatch,
280                },
281                _ => MatcherResult::NoMatch,
282            }
283        }
284        fn describe(&self, r: MatcherResult) -> Description {
285            self.inner.describe(r)
286        }
287        fn explain_match(&self, actual: &Value) -> Description {
288            match actual {
289                Value::Number(n) => match n.as_i64() {
290                    Some(i) => match i32::try_from(i) {
291                        Ok(i32_val) => self.inner.explain_match(i32_val),
292                        Err(_) => Description::new().text(format!("number out of i32 range: {n}")),
293                    },
294                    None => Description::new().text(format!("number out of i64 range: {n}")),
295                },
296                _ => Description::new().text("which is not a JSON number"),
297            }
298        }
299    }
300
301    impl<M> IntoJsonMatcher<i32> for M
302    where
303        M: Matcher<i32> + 'static,
304    {
305        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
306            Box::new(JsonPrimitiveMatcher::<M, i32>::new(self))
307        }
308    }
309
310    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i8>
311    where
312        M: Matcher<i8>,
313    {
314        fn matches(&self, actual: &Value) -> MatcherResult {
315            match actual {
316                Value::Number(n) => match n.as_i64() {
317                    Some(i) => match i8::try_from(i) {
318                        Ok(i8_val) => self.inner.matches(i8_val),
319                        Err(_) => MatcherResult::NoMatch,
320                    },
321                    None => MatcherResult::NoMatch,
322                },
323                _ => MatcherResult::NoMatch,
324            }
325        }
326        fn describe(&self, r: MatcherResult) -> Description {
327            self.inner.describe(r)
328        }
329        fn explain_match(&self, actual: &Value) -> Description {
330            match actual {
331                Value::Number(n) => match n.as_i64() {
332                    Some(i) => match i8::try_from(i) {
333                        Ok(i8_val) => self.inner.explain_match(i8_val),
334                        Err(_) => Description::new().text(format!("number out of i8 range: {n}")),
335                    },
336                    None => Description::new().text(format!("number out of i64 range: {n}")),
337                },
338                _ => Description::new().text("which is not a JSON number"),
339            }
340        }
341    }
342
343    impl<M> IntoJsonMatcher<i8> for M
344    where
345        M: Matcher<i8> + 'static,
346    {
347        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
348            Box::new(JsonPrimitiveMatcher::<M, i8>::new(self))
349        }
350    }
351
352    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i16>
353    where
354        M: Matcher<i16>,
355    {
356        fn matches(&self, actual: &Value) -> MatcherResult {
357            match actual {
358                Value::Number(n) => match n.as_i64() {
359                    Some(i) => match i16::try_from(i) {
360                        Ok(i16_val) => self.inner.matches(i16_val),
361                        Err(_) => MatcherResult::NoMatch,
362                    },
363                    None => MatcherResult::NoMatch,
364                },
365                _ => MatcherResult::NoMatch,
366            }
367        }
368        fn describe(&self, r: MatcherResult) -> Description {
369            self.inner.describe(r)
370        }
371        fn explain_match(&self, actual: &Value) -> Description {
372            match actual {
373                Value::Number(n) => match n.as_i64() {
374                    Some(i) => match i16::try_from(i) {
375                        Ok(i16_val) => self.inner.explain_match(i16_val),
376                        Err(_) => Description::new().text(format!("number out of i16 range: {n}")),
377                    },
378                    None => Description::new().text(format!("number out of i64 range: {n}")),
379                },
380                _ => Description::new().text("which is not a JSON number"),
381            }
382        }
383    }
384
385    impl<M> IntoJsonMatcher<i16> for M
386    where
387        M: Matcher<i16> + 'static,
388    {
389        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
390            Box::new(JsonPrimitiveMatcher::<M, i16>::new(self))
391        }
392    }
393
394    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, u8>
395    where
396        M: Matcher<u8>,
397    {
398        fn matches(&self, actual: &Value) -> MatcherResult {
399            match actual {
400                Value::Number(n) => match n.as_u64() {
401                    Some(u) => match u8::try_from(u) {
402                        Ok(u8_val) => self.inner.matches(u8_val),
403                        Err(_) => MatcherResult::NoMatch,
404                    },
405                    None => MatcherResult::NoMatch,
406                },
407                _ => MatcherResult::NoMatch,
408            }
409        }
410        fn describe(&self, r: MatcherResult) -> Description {
411            self.inner.describe(r)
412        }
413        fn explain_match(&self, actual: &Value) -> Description {
414            match actual {
415                Value::Number(n) => match n.as_u64() {
416                    Some(u) => match u8::try_from(u) {
417                        Ok(u8_val) => self.inner.explain_match(u8_val),
418                        Err(_) => Description::new().text(format!("number out of u8 range: {n}")),
419                    },
420                    None => Description::new().text(format!("number out of u64 range: {n}")),
421                },
422                _ => Description::new().text("which is not a JSON number"),
423            }
424        }
425    }
426
427    impl<M> IntoJsonMatcher<u8> for M
428    where
429        M: Matcher<u8> + 'static,
430    {
431        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
432            Box::new(JsonPrimitiveMatcher::<M, u8>::new(self))
433        }
434    }
435
436    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, u16>
437    where
438        M: Matcher<u16>,
439    {
440        fn matches(&self, actual: &Value) -> MatcherResult {
441            match actual {
442                Value::Number(n) => match n.as_u64() {
443                    Some(u) => match u16::try_from(u) {
444                        Ok(u16_val) => self.inner.matches(u16_val),
445                        Err(_) => MatcherResult::NoMatch,
446                    },
447                    None => MatcherResult::NoMatch,
448                },
449                _ => MatcherResult::NoMatch,
450            }
451        }
452        fn describe(&self, r: MatcherResult) -> Description {
453            self.inner.describe(r)
454        }
455        fn explain_match(&self, actual: &Value) -> Description {
456            match actual {
457                Value::Number(n) => match n.as_u64() {
458                    Some(u) => match u16::try_from(u) {
459                        Ok(u16_val) => self.inner.explain_match(u16_val),
460                        Err(_) => Description::new().text(format!("number out of u16 range: {n}")),
461                    },
462                    None => Description::new().text(format!("number out of u64 range: {n}")),
463                },
464                _ => Description::new().text("which is not a JSON number"),
465            }
466        }
467    }
468
469    impl<M> IntoJsonMatcher<u16> for M
470    where
471        M: Matcher<u16> + 'static,
472    {
473        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
474            Box::new(JsonPrimitiveMatcher::<M, u16>::new(self))
475        }
476    }
477
478    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, u32>
479    where
480        M: Matcher<u32>,
481    {
482        fn matches(&self, actual: &Value) -> MatcherResult {
483            match actual {
484                Value::Number(n) => match n.as_u64() {
485                    Some(u) => match u32::try_from(u) {
486                        Ok(u32_val) => self.inner.matches(u32_val),
487                        Err(_) => MatcherResult::NoMatch,
488                    },
489                    None => MatcherResult::NoMatch,
490                },
491                _ => MatcherResult::NoMatch,
492            }
493        }
494        fn describe(&self, r: MatcherResult) -> Description {
495            self.inner.describe(r)
496        }
497        fn explain_match(&self, actual: &Value) -> Description {
498            match actual {
499                Value::Number(n) => match n.as_u64() {
500                    Some(u) => match u32::try_from(u) {
501                        Ok(u32_val) => self.inner.explain_match(u32_val),
502                        Err(_) => Description::new().text(format!("number out of u32 range: {n}")),
503                    },
504                    None => Description::new().text(format!("number out of u64 range: {n}")),
505                },
506                _ => Description::new().text("which is not a JSON number"),
507            }
508        }
509    }
510
511    impl<M> IntoJsonMatcher<u32> for M
512    where
513        M: Matcher<u32> + 'static,
514    {
515        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
516            Box::new(JsonPrimitiveMatcher::<M, u32>::new(self))
517        }
518    }
519
520    // usize support
521    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, usize>
522    where
523        M: Matcher<usize>,
524    {
525        fn matches(&self, actual: &Value) -> MatcherResult {
526            match actual {
527                Value::Number(n) => match n.as_u64() {
528                    Some(u) => match usize::try_from(u) {
529                        Ok(usize_val) => self.inner.matches(usize_val),
530                        Err(_) => MatcherResult::NoMatch,
531                    },
532                    None => MatcherResult::NoMatch,
533                },
534                _ => MatcherResult::NoMatch,
535            }
536        }
537
538        fn describe(&self, r: MatcherResult) -> Description {
539            self.inner.describe(r)
540        }
541
542        fn explain_match(&self, actual: &Value) -> Description {
543            match actual {
544                Value::Number(n) => match n.as_u64() {
545                    Some(u) => match usize::try_from(u) {
546                        Ok(usize_val) => self.inner.explain_match(usize_val),
547                        Err(_) => {
548                            Description::new().text(format!("number out of usize range: {n}"))
549                        }
550                    },
551                    None => Description::new().text(format!("number not convertible to u64: {n}")),
552                },
553                _ => Description::new().text("which is not a JSON number"),
554            }
555        }
556    }
557
558    impl<M> IntoJsonMatcher<usize> for M
559    where
560        M: Matcher<usize> + 'static,
561    {
562        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
563            Box::new(JsonPrimitiveMatcher::<M, usize>::new(self))
564        }
565    }
566}