googletest_json_serde/matchers/
primitive_matcher.rs

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