googletest_json_serde/matchers/
json_matcher.rs

1//! JSON predicate matchers for googletest assertions.
2//!
3//! # Examples
4//! ```rust
5//! # use googletest::prelude::*;
6//! # use googletest_json_serde::json;
7//! # use serde_json::json as j;
8//! assert_that!(j!("hi"), json::is_string());
9//! ```
10
11use crate::matchers::__internal_unstable_do_not_depend_on_these;
12use crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPredicateMatcher;
13use googletest::description::Description;
14use serde_json::Value;
15
16/// Builds a JSON matcher from an arbitrary predicate function.
17///
18/// # Examples
19///
20/// ```rust
21/// # use googletest::prelude::*;
22/// # use googletest_json_serde::json;
23/// # use serde_json::json as j;
24/// let positive = json::predicate(|v| v.as_i64().is_some_and(|n| n > 0));
25/// verify_that!(j!(42), &positive);
26/// assert_that!(j!(-1), not(&positive));
27/// ```
28pub fn predicate<P>(
29    predicate: P,
30) -> JsonPredicateMatcher<
31    P,
32    __internal_unstable_do_not_depend_on_these::NoDescription,
33    __internal_unstable_do_not_depend_on_these::NoDescription,
34>
35where
36    P: Fn(&Value) -> bool,
37{
38    JsonPredicateMatcher::new(
39        predicate,
40        __internal_unstable_do_not_depend_on_these::NoDescription,
41        __internal_unstable_do_not_depend_on_these::NoDescription,
42    )
43}
44/// Matches JSON null values.
45///
46/// # Examples
47///
48/// ```rust
49/// # use googletest::prelude::*;
50/// # use googletest_json_serde::json;
51/// # use serde_json::json as j;
52/// assert_that!(j!(null), json::is_null());
53/// assert_that!(j!("value"), not(json::is_null()));
54/// ```
55pub fn is_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
56    JsonPredicateMatcher::new(|v| v.is_null(), "JSON null", "which is not JSON null")
57        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
58}
59/// Matches JSON values that are not null.
60///
61/// # Examples
62///
63/// ```rust
64/// # use googletest::prelude::*;
65/// # use googletest_json_serde::json;
66/// # use serde_json::json as j;
67/// assert_that!(j!("text"), json::is_not_null());
68/// assert_that!(j!(null), not(json::is_not_null()));
69/// ```
70pub fn is_not_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
71    JsonPredicateMatcher::new(|v| !v.is_null(), "not JSON null", "which is JSON null")
72        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
73}
74
75/// Matches JSON values that are not null.
76///
77/// # Examples
78///
79/// ```rust
80/// # use googletest::prelude::*;
81/// # use googletest_json_serde::json;
82/// # use serde_json::json as j;
83/// assert_that!(j!("ok"), json::any_value());
84/// assert_that!(j!(null), not(json::any_value()));
85/// ```
86#[deprecated(since = "0.2.2", note = "Use `is_not_null` instead")]
87pub fn any_value() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
88    JsonPredicateMatcher::new(|v| !v.is_null(), "any JSON value", "is not any JSON value")
89        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
90}
91
92/// Matches JSON string values.
93///
94/// # Examples
95///
96/// ```rust
97/// # use googletest::prelude::*;
98/// # use googletest_json_serde::json;
99/// # use serde_json::json as j;
100/// assert_that!(j!("hi"), json::is_string());
101/// assert_that!(j!(true), not(json::is_string()));
102/// ```
103pub fn is_string() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
104    JsonPredicateMatcher::new(
105        |v| v.is_string(),
106        "a JSON string",
107        "which is not a JSON string",
108    )
109    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
110}
111
112/// Matches JSON number values.
113///
114/// # Examples
115///
116/// ```rust
117/// # use googletest::prelude::*;
118/// # use googletest_json_serde::json;
119/// # use serde_json::json as j;
120/// assert_that!(j!(3.14), json::is_number());
121/// assert_that!(j!("three"), not(json::is_number()));
122/// ```
123pub fn is_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
124    JsonPredicateMatcher::new(
125        |v| v.is_number(),
126        "a JSON number",
127        "which is not a JSON number",
128    )
129    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
130}
131
132/// Matches JSON numbers that are integers.
133///
134/// # Examples
135///
136/// ```rust
137/// # use googletest::prelude::*;
138/// # use googletest_json_serde::json;
139/// # use serde_json::json as j;
140/// assert_that!(j!(42), json::is_integer());
141/// assert_that!(j!(3.14), not(json::is_integer()));
142/// ```
143pub fn is_integer() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
144    JsonPredicateMatcher::new(
145        |v| matches!(v, Value::Number(n) if n.is_i64() || n.is_u64()),
146        "an integer JSON number",
147        "which is not an integer JSON number",
148    )
149    .with_explain_fn(|v| {
150        if matches!(v, Value::Number(_)) {
151            Description::new().text("which is a non-integer JSON number")
152        } else {
153            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
154        }
155    })
156}
157
158/// Matches JSON numbers whose fractional part is zero (e.g., `2` or `2.0`).
159///
160/// # Examples
161///
162/// ```rust
163/// # use googletest::prelude::*;
164/// # use googletest_json_serde::json;
165/// # use serde_json::json as j;
166/// assert_that!(j!(2.0), json::is_whole_number());
167/// assert_that!(j!(2.5), not(json::is_whole_number()));
168/// ```
169pub fn is_whole_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
170{
171    JsonPredicateMatcher::new(
172        |v| match v {
173            Value::Number(n) => {
174                if n.is_i64() || n.is_u64() {
175                    true
176                } else {
177                    n.as_f64()
178                        .is_some_and(|f| f.is_finite() && f.fract() == 0.0)
179                }
180            }
181            _ => false,
182        },
183        "a JSON number with no fractional part",
184        "which is not a JSON number with no fractional part",
185    )
186    .with_explain_fn(|v| {
187        if matches!(v, Value::Number(_)) {
188            Description::new().text("which is a JSON number with a fractional part")
189        } else {
190            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
191        }
192    })
193}
194
195/// Matches JSON numbers that have a non-zero fractional part.
196///
197/// # Examples
198///
199/// ```rust
200/// # use googletest::prelude::*;
201/// # use googletest_json_serde::json;
202/// # use serde_json::json as j;
203/// assert_that!(j!(3.5), json::is_fractional_number());
204/// assert_that!(j!(3), not(json::is_fractional_number()));
205/// assert_that!(j!(3.0), not(json::is_fractional_number()));
206/// ```
207pub fn is_fractional_number()
208-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
209    JsonPredicateMatcher::new(
210        |v| match v {
211            Value::Number(n) => {
212                if n.is_i64() || n.is_u64() {
213                    return false;
214                }
215                n.as_f64()
216                    .is_some_and(|f| f.is_finite() && f.fract() != 0.0)
217            }
218            _ => false,
219        },
220        "a JSON number with a fractional part",
221        "which is not a JSON number with a fractional part",
222    )
223    .with_explain_fn(|v| {
224        if matches!(v, Value::Number(_)) {
225            Description::new().text("which is a JSON number without a fractional part")
226        } else {
227            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
228        }
229    })
230}
231
232/// Matches JSON boolean values.
233///
234/// # Examples
235///
236/// ```rust
237/// # use googletest::prelude::*;
238/// # use googletest_json_serde::json;
239/// # use serde_json::json as j;
240/// assert_that!(j!(true), json::is_boolean());
241/// assert_that!(j!(0), not(json::is_boolean()));
242/// ```
243pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
244    JsonPredicateMatcher::new(
245        |v| v.is_boolean(),
246        "a JSON boolean",
247        "which is not a JSON boolean",
248    )
249    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
250}
251
252/// Matches the JSON boolean `true` value.
253///
254/// # Examples
255///
256/// ```rust
257/// # use googletest::prelude::*;
258/// # use googletest_json_serde::json;
259/// # use serde_json::json as j;
260/// assert_that!(j!(true), json::is_true());
261/// assert_that!(j!(false), not(json::is_true()));
262/// ```
263pub fn is_true() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
264    JsonPredicateMatcher::new(
265        |v| matches!(v, Value::Bool(true)),
266        "JSON true",
267        "which is not JSON true",
268    )
269    .with_explain_fn(|v| match v {
270        Value::Bool(false) => Description::new().text("which is JSON false"),
271        _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
272    })
273}
274
275/// Matches the JSON boolean `false` value.
276///
277/// # Examples
278///
279/// ```rust
280/// # use googletest::prelude::*;
281/// # use googletest_json_serde::json;
282/// # use serde_json::json as j;
283/// assert_that!(j!(false), json::is_false());
284/// assert_that!(j!(true), not(json::is_false()));
285/// ```
286pub fn is_false() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
287    JsonPredicateMatcher::new(
288        |v| matches!(v, Value::Bool(false)),
289        "JSON false",
290        "which is not JSON false",
291    )
292    .with_explain_fn(|v| match v {
293        Value::Bool(true) => Description::new().text("which is JSON true"),
294        _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
295    })
296}
297
298/// Matches JSON array values.
299///
300/// # Examples
301///
302/// ```rust
303/// # use googletest::prelude::*;
304/// # use googletest_json_serde::json;
305/// # use serde_json::json as j;
306/// assert_that!(j!([1, 2]), json::is_array());
307/// assert_that!(j!({"a":1}), not(json::is_array()));
308/// ```
309pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
310    JsonPredicateMatcher::new(
311        |v| v.is_array(),
312        "a JSON array",
313        "which is not a JSON array",
314    )
315    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
316}
317
318/// Matches an empty JSON array (`[]`).
319///
320/// # Examples
321///
322/// ```rust
323/// # use googletest::prelude::*;
324/// # use googletest_json_serde::json;
325/// # use serde_json::json as j;
326/// assert_that!(j!([]), json::is_empty_array());
327/// assert_that!(j!([1]), not(json::is_empty_array()));
328/// ```
329pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
330{
331    JsonPredicateMatcher::new(
332        |v| v.as_array().is_some_and(|a| a.is_empty()),
333        "an empty JSON array",
334        "which is not an empty JSON array",
335    )
336    .with_explain_fn(|v| {
337        if v.is_array() {
338            Description::new().text("which is a non-empty JSON array")
339        } else {
340            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
341        }
342    })
343}
344
345/// Matches JSON object values.
346///
347/// # Examples
348///
349/// ```rust
350/// # use googletest::prelude::*;
351/// # use googletest_json_serde::json;
352/// # use serde_json::json as j;
353/// assert_that!(j!({"a": 1}), json::is_object());
354/// assert_that!(j!(null), not(json::is_object()));
355/// ```
356pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
357    JsonPredicateMatcher::new(
358        |v| v.is_object(),
359        "a JSON object",
360        "which is not a JSON object",
361    )
362    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
363}
364
365/// Matches an empty JSON object (`{}`).
366///
367/// # Examples
368///
369/// ```rust
370/// # use googletest::prelude::*;
371/// # use googletest_json_serde::json;
372/// # use serde_json::json as j;
373/// assert_that!(j!({}), json::is_empty_object());
374/// assert_that!(j!({"a":1}), not(json::is_empty_object()));
375/// ```
376pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
377{
378    JsonPredicateMatcher::new(
379        |v| v.as_object().is_some_and(|o| o.is_empty()),
380        "an empty JSON object",
381        "which is not an empty JSON object",
382    )
383    .with_explain_fn(|v| {
384        if v.is_object() {
385            Description::new().text("which is a non-empty JSON object")
386        } else {
387            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
388        }
389    })
390}
391
392// Path-based matchers live in `path_matcher.rs`.
393
394#[doc(hidden)]
395pub mod internal {
396    use googletest::description::Description;
397    use googletest::matcher::MatcherResult::{Match, NoMatch};
398    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
399    use serde_json::Value;
400
401    /// Trait for types that can provide a description string.
402    pub trait PredicateDescription {
403        fn to_description(self) -> String;
404    }
405
406    impl PredicateDescription for &'static str {
407        fn to_description(self) -> String {
408            self.to_string()
409        }
410    }
411
412    impl PredicateDescription for String {
413        fn to_description(self) -> String {
414            self
415        }
416    }
417
418    impl<F> PredicateDescription for F
419    where
420        F: Fn() -> String,
421    {
422        fn to_description(self) -> String {
423            self()
424        }
425    }
426    /// Sentinel type for missing descriptions.
427    #[derive(Clone, Copy, Debug)]
428    pub struct NoDescription;
429    impl PredicateDescription for NoDescription {
430        fn to_description(self) -> String {
431            String::new()
432        }
433    }
434
435    /// Type alias for the explain function to reduce type complexity.
436    type ExplainFn = Box<dyn Fn(&Value) -> Description>;
437
438    #[derive(MatcherBase)]
439    pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
440    where
441        P: Fn(&Value) -> bool,
442        D1: PredicateDescription,
443        D2: PredicateDescription,
444    {
445        predicate: P,
446        positive_description: D1,
447        negative_description: D2,
448        explain_fn: Option<ExplainFn>,
449    }
450
451    impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
452    where
453        P: Fn(&Value) -> bool,
454        D1: PredicateDescription,
455        D2: PredicateDescription,
456    {
457        pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
458            Self {
459                predicate,
460                positive_description,
461                negative_description,
462                explain_fn: None,
463            }
464        }
465
466        pub fn with_description<D1b, D2b>(
467            self,
468            positive_description: D1b,
469            negative_description: D2b,
470        ) -> JsonPredicateMatcher<P, D1b, D2b>
471        where
472            D1b: PredicateDescription,
473            D2b: PredicateDescription,
474        {
475            JsonPredicateMatcher {
476                predicate: self.predicate,
477                positive_description,
478                negative_description,
479                explain_fn: self.explain_fn,
480            }
481        }
482
483        pub fn with_explain_fn<F>(mut self, f: F) -> Self
484        where
485            F: Fn(&Value) -> Description + 'static,
486        {
487            self.explain_fn = Some(Box::new(f));
488            self
489        }
490    }
491
492    impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
493    where
494        P: Fn(&Value) -> bool,
495        D1: PredicateDescription + Clone,
496        D2: PredicateDescription + Clone,
497    {
498        fn matches(&self, actual: &Value) -> MatcherResult {
499            if (self.predicate)(actual) {
500                Match
501            } else {
502                NoMatch
503            }
504        }
505
506        fn describe(&self, result: MatcherResult) -> Description {
507            let pos = self.positive_description.clone().to_description();
508            let neg = self.negative_description.clone().to_description();
509
510            match result {
511                Match if pos.is_empty() => "matches predicate".into(),
512                NoMatch if neg.is_empty() => "does not match predicate".into(),
513                Match => pos.into(),
514                NoMatch => neg.into(),
515            }
516        }
517
518        fn explain_match(&self, actual: &Value) -> Description {
519            if let Some(ref f) = self.explain_fn {
520                return f(actual);
521            }
522            Description::new().text("which does not match the predicate")
523        }
524    }
525    /// Marker trait for JSON-aware matchers.
526    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
527        /// Returns true if this matcher allows the field to be missing in an object.
528        fn allows_missing(&self) -> bool {
529            false
530        }
531    }
532
533    /// Trait for converting into a boxed JSON matcher.
534    pub trait IntoJsonMatcher<T> {
535        fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
536    }
537
538    impl<J> IntoJsonMatcher<()> for J
539    where
540        J: JsonMatcher + 'static,
541    {
542        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
543            Box::new(self)
544        }
545    }
546
547    // A concrete matcher that checks equality with an owned serde_json::Value.
548    // This avoids lifetime issues of using googletest::eq on &Value and gives
549    // us control over descriptions.
550    #[derive(googletest::matcher::MatcherBase)]
551    struct JsonEqMatcher {
552        expected: Value,
553    }
554
555    impl Matcher<&Value> for JsonEqMatcher {
556        fn matches(&self, actual: &Value) -> MatcherResult {
557            if *actual == self.expected {
558                Match
559            } else {
560                NoMatch
561            }
562        }
563
564        fn describe(&self, result: MatcherResult) -> Description {
565            match result {
566                Match => format!("is equal to {:?}", self.expected).into(),
567                NoMatch => format!("isn't equal to {:?}", self.expected).into(),
568            }
569        }
570
571        fn explain_match(&self, _actual: &Value) -> Description {
572            // Framework prints the actual value already. Provide the expected.
573            format!("which isn't equal to {:?}", self.expected).into()
574        }
575    }
576
577    impl JsonMatcher for JsonEqMatcher {}
578
579    // Allow &serde_json::Value to be used seamlessly with JSON macros
580    impl IntoJsonMatcher<Value> for &Value {
581        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
582            Box::new(JsonEqMatcher {
583                expected: self.clone(),
584            })
585        }
586    }
587
588    impl IntoJsonMatcher<Value> for Value {
589        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
590            Box::new(JsonEqMatcher { expected: self })
591        }
592    }
593
594    // Literal support marker type
595    pub struct Literal;
596
597    impl IntoJsonMatcher<Literal> for &str {
598        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
599            Box::new(JsonEqMatcher {
600                expected: Value::from(self),
601            })
602        }
603    }
604
605    impl IntoJsonMatcher<Literal> for String {
606        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
607            Box::new(JsonEqMatcher {
608                expected: Value::from(self),
609            })
610        }
611    }
612
613    impl IntoJsonMatcher<Literal> for bool {
614        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
615            Box::new(JsonEqMatcher {
616                expected: Value::from(self),
617            })
618        }
619    }
620
621    impl IntoJsonMatcher<Literal> for i64 {
622        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
623            Box::new(JsonEqMatcher {
624                expected: Value::from(self),
625            })
626        }
627    }
628    impl IntoJsonMatcher<Literal> for i32 {
629        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
630            Box::new(JsonEqMatcher {
631                expected: Value::from(self),
632            })
633        }
634    }
635
636    impl IntoJsonMatcher<Literal> for u64 {
637        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
638            Box::new(JsonEqMatcher {
639                expected: Value::from(self),
640            })
641        }
642    }
643
644    impl IntoJsonMatcher<Literal> for f64 {
645        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
646            Box::new(JsonEqMatcher {
647                expected: Value::from(self),
648            })
649        }
650    }
651
652    impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
653    where
654        P: Fn(&Value) -> bool + 'static,
655        D1: PredicateDescription + Clone + 'static,
656        D2: PredicateDescription + Clone + 'static,
657    {
658    }
659
660    pub fn describe_json_type(v: &Value) -> Description {
661        match v {
662            Value::Null => "which is a JSON null",
663            Value::String(_) => "which is a JSON string",
664            Value::Number(_) => "which is a JSON number",
665            Value::Bool(_) => "which is a JSON boolean",
666            Value::Array(_) => "which is a JSON array",
667            Value::Object(_) => "which is a JSON object",
668        }
669        .into()
670    }
671}