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 boolean values.
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!(true), json::is_boolean());
141/// assert_that!(j!(0), not(json::is_boolean()));
142/// ```
143pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
144    JsonPredicateMatcher::new(
145        |v| v.is_boolean(),
146        "a JSON boolean",
147        "which is not a JSON boolean",
148    )
149    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
150}
151
152/// Matches the JSON boolean `true` value.
153///
154/// # Examples
155///
156/// ```rust
157/// # use googletest::prelude::*;
158/// # use googletest_json_serde::json;
159/// # use serde_json::json as j;
160/// assert_that!(j!(true), json::is_true());
161/// assert_that!(j!(false), not(json::is_true()));
162/// ```
163pub fn is_true() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
164    JsonPredicateMatcher::new(
165        |v| matches!(v, Value::Bool(true)),
166        "JSON true",
167        "which is not JSON true",
168    )
169    .with_explain_fn(|v| match v {
170        Value::Bool(false) => Description::new().text("which is JSON false"),
171        _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
172    })
173}
174
175/// Matches the JSON boolean `false` value.
176///
177/// # Examples
178///
179/// ```rust
180/// # use googletest::prelude::*;
181/// # use googletest_json_serde::json;
182/// # use serde_json::json as j;
183/// assert_that!(j!(false), json::is_false());
184/// assert_that!(j!(true), not(json::is_false()));
185/// ```
186pub fn is_false() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
187    JsonPredicateMatcher::new(
188        |v| matches!(v, Value::Bool(false)),
189        "JSON false",
190        "which is not JSON false",
191    )
192    .with_explain_fn(|v| match v {
193        Value::Bool(true) => Description::new().text("which is JSON true"),
194        _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
195    })
196}
197
198/// Matches JSON array values.
199///
200/// # Examples
201///
202/// ```rust
203/// # use googletest::prelude::*;
204/// # use googletest_json_serde::json;
205/// # use serde_json::json as j;
206/// assert_that!(j!([1, 2]), json::is_array());
207/// assert_that!(j!({"a":1}), not(json::is_array()));
208/// ```
209pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
210    JsonPredicateMatcher::new(
211        |v| v.is_array(),
212        "a JSON array",
213        "which is not a JSON array",
214    )
215    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
216}
217
218/// Matches an empty JSON array (`[]`).
219///
220/// # Examples
221///
222/// ```rust
223/// # use googletest::prelude::*;
224/// # use googletest_json_serde::json;
225/// # use serde_json::json as j;
226/// assert_that!(j!([]), json::is_empty_array());
227/// assert_that!(j!([1]), not(json::is_empty_array()));
228/// ```
229pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
230{
231    JsonPredicateMatcher::new(
232        |v| v.as_array().is_some_and(|a| a.is_empty()),
233        "an empty JSON array",
234        "which is not an empty JSON array",
235    )
236    .with_explain_fn(|v| {
237        if v.is_array() {
238            Description::new().text("which is a non-empty JSON array")
239        } else {
240            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
241        }
242    })
243}
244
245/// Matches JSON object values.
246///
247/// # Examples
248///
249/// ```rust
250/// # use googletest::prelude::*;
251/// # use googletest_json_serde::json;
252/// # use serde_json::json as j;
253/// assert_that!(j!({"a": 1}), json::is_object());
254/// assert_that!(j!(null), not(json::is_object()));
255/// ```
256pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
257    JsonPredicateMatcher::new(
258        |v| v.is_object(),
259        "a JSON object",
260        "which is not a JSON object",
261    )
262    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
263}
264
265/// Matches an empty JSON object (`{}`).
266///
267/// # Examples
268///
269/// ```rust
270/// # use googletest::prelude::*;
271/// # use googletest_json_serde::json;
272/// # use serde_json::json as j;
273/// assert_that!(j!({}), json::is_empty_object());
274/// assert_that!(j!({"a":1}), not(json::is_empty_object()));
275/// ```
276pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
277{
278    JsonPredicateMatcher::new(
279        |v| v.as_object().is_some_and(|o| o.is_empty()),
280        "an empty JSON object",
281        "which is not an empty JSON object",
282    )
283    .with_explain_fn(|v| {
284        if v.is_object() {
285            Description::new().text("which is a non-empty JSON object")
286        } else {
287            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
288        }
289    })
290}
291
292// Path-based matchers live in `path_matcher.rs`.
293
294#[doc(hidden)]
295pub mod internal {
296    use googletest::description::Description;
297    use googletest::matcher::MatcherResult::{Match, NoMatch};
298    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
299    use serde_json::Value;
300
301    /// Trait for types that can provide a description string.
302    pub trait PredicateDescription {
303        fn to_description(self) -> String;
304    }
305
306    impl PredicateDescription for &'static str {
307        fn to_description(self) -> String {
308            self.to_string()
309        }
310    }
311
312    impl PredicateDescription for String {
313        fn to_description(self) -> String {
314            self
315        }
316    }
317
318    impl<F> PredicateDescription for F
319    where
320        F: Fn() -> String,
321    {
322        fn to_description(self) -> String {
323            self()
324        }
325    }
326    /// Sentinel type for missing descriptions.
327    #[derive(Clone, Copy, Debug)]
328    pub struct NoDescription;
329    impl PredicateDescription for NoDescription {
330        fn to_description(self) -> String {
331            String::new()
332        }
333    }
334
335    /// Type alias for the explain function to reduce type complexity.
336    type ExplainFn = Box<dyn Fn(&Value) -> Description>;
337
338    #[derive(MatcherBase)]
339    pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
340    where
341        P: Fn(&Value) -> bool,
342        D1: PredicateDescription,
343        D2: PredicateDescription,
344    {
345        predicate: P,
346        positive_description: D1,
347        negative_description: D2,
348        explain_fn: Option<ExplainFn>,
349    }
350
351    impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
352    where
353        P: Fn(&Value) -> bool,
354        D1: PredicateDescription,
355        D2: PredicateDescription,
356    {
357        pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
358            Self {
359                predicate,
360                positive_description,
361                negative_description,
362                explain_fn: None,
363            }
364        }
365
366        pub fn with_description<D1b, D2b>(
367            self,
368            positive_description: D1b,
369            negative_description: D2b,
370        ) -> JsonPredicateMatcher<P, D1b, D2b>
371        where
372            D1b: PredicateDescription,
373            D2b: PredicateDescription,
374        {
375            JsonPredicateMatcher {
376                predicate: self.predicate,
377                positive_description,
378                negative_description,
379                explain_fn: self.explain_fn,
380            }
381        }
382
383        pub fn with_explain_fn<F>(mut self, f: F) -> Self
384        where
385            F: Fn(&Value) -> Description + 'static,
386        {
387            self.explain_fn = Some(Box::new(f));
388            self
389        }
390    }
391
392    impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
393    where
394        P: Fn(&Value) -> bool,
395        D1: PredicateDescription + Clone,
396        D2: PredicateDescription + Clone,
397    {
398        fn matches(&self, actual: &Value) -> MatcherResult {
399            if (self.predicate)(actual) {
400                Match
401            } else {
402                NoMatch
403            }
404        }
405
406        fn describe(&self, result: MatcherResult) -> Description {
407            let pos = self.positive_description.clone().to_description();
408            let neg = self.negative_description.clone().to_description();
409
410            match result {
411                Match if pos.is_empty() => "matches predicate".into(),
412                NoMatch if neg.is_empty() => "does not match predicate".into(),
413                Match => pos.into(),
414                NoMatch => neg.into(),
415            }
416        }
417
418        fn explain_match(&self, actual: &Value) -> Description {
419            if let Some(ref f) = self.explain_fn {
420                return f(actual);
421            }
422            Description::new().text("which does not match the predicate")
423        }
424    }
425    /// Marker trait for JSON-aware matchers.
426    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
427        /// Returns true if this matcher allows the field to be missing in an object.
428        fn allows_missing(&self) -> bool {
429            false
430        }
431    }
432
433    /// Trait for converting into a boxed JSON matcher.
434    pub trait IntoJsonMatcher<T> {
435        fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
436    }
437
438    impl<J> IntoJsonMatcher<()> for J
439    where
440        J: JsonMatcher + 'static,
441    {
442        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
443            Box::new(self)
444        }
445    }
446
447    // A concrete matcher that checks equality with an owned serde_json::Value.
448    // This avoids lifetime issues of using googletest::eq on &Value and gives
449    // us control over descriptions.
450    #[derive(googletest::matcher::MatcherBase)]
451    struct JsonEqMatcher {
452        expected: Value,
453    }
454
455    impl Matcher<&Value> for JsonEqMatcher {
456        fn matches(&self, actual: &Value) -> MatcherResult {
457            if *actual == self.expected {
458                Match
459            } else {
460                NoMatch
461            }
462        }
463
464        fn describe(&self, result: MatcherResult) -> Description {
465            match result {
466                Match => format!("is equal to {:?}", self.expected).into(),
467                NoMatch => format!("isn't equal to {:?}", self.expected).into(),
468            }
469        }
470
471        fn explain_match(&self, _actual: &Value) -> Description {
472            // Framework prints the actual value already. Provide the expected.
473            format!("which isn't equal to {:?}", self.expected).into()
474        }
475    }
476
477    impl JsonMatcher for JsonEqMatcher {}
478
479    // Allow &serde_json::Value to be used seamlessly with JSON macros
480    impl IntoJsonMatcher<Value> for &Value {
481        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
482            Box::new(JsonEqMatcher {
483                expected: self.clone(),
484            })
485        }
486    }
487
488    impl IntoJsonMatcher<Value> for Value {
489        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
490            Box::new(JsonEqMatcher { expected: self })
491        }
492    }
493
494    // Literal support marker type
495    pub struct Literal;
496
497    impl IntoJsonMatcher<Literal> for &str {
498        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
499            Box::new(JsonEqMatcher {
500                expected: Value::from(self),
501            })
502        }
503    }
504
505    impl IntoJsonMatcher<Literal> for String {
506        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
507            Box::new(JsonEqMatcher {
508                expected: Value::from(self),
509            })
510        }
511    }
512
513    impl IntoJsonMatcher<Literal> for bool {
514        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
515            Box::new(JsonEqMatcher {
516                expected: Value::from(self),
517            })
518        }
519    }
520
521    impl IntoJsonMatcher<Literal> for i64 {
522        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
523            Box::new(JsonEqMatcher {
524                expected: Value::from(self),
525            })
526        }
527    }
528    impl IntoJsonMatcher<Literal> for i32 {
529        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
530            Box::new(JsonEqMatcher {
531                expected: Value::from(self),
532            })
533        }
534    }
535
536    impl IntoJsonMatcher<Literal> for u64 {
537        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
538            Box::new(JsonEqMatcher {
539                expected: Value::from(self),
540            })
541        }
542    }
543
544    impl IntoJsonMatcher<Literal> for f64 {
545        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
546            Box::new(JsonEqMatcher {
547                expected: Value::from(self),
548            })
549        }
550    }
551
552    impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
553    where
554        P: Fn(&Value) -> bool + 'static,
555        D1: PredicateDescription + Clone + 'static,
556        D2: PredicateDescription + Clone + 'static,
557    {
558    }
559
560    pub fn describe_json_type(v: &Value) -> Description {
561        match v {
562            Value::Null => "which is a JSON null",
563            Value::String(_) => "which is a JSON string",
564            Value::Number(_) => "which is a JSON number",
565            Value::Bool(_) => "which is a JSON boolean",
566            Value::Array(_) => "which is a JSON array",
567            Value::Object(_) => "which is a JSON object",
568        }
569        .into()
570    }
571}