googletest_json_serde/matchers/
json_matcher.rs

1//! Utility matchers and macros for concise JSON assertions using googletest.
2
3use crate::json::__internal_unstable_do_not_depend_on_these;
4use crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPredicateMatcher;
5use googletest::description::Description;
6use serde_json::Value;
7
8/// Creates a custom JSON matcher from an arbitrary predicate function.
9///
10/// This function allows defining ad-hoc JSON matchers inline by supplying a closure or function
11/// that returns `true` for matching values.
12/// The resulting matcher can optionally be extended with:
13/// - `.with_description("expected", "not expected")` — to provide custom messages, and
14/// - `.with_explain_fn(|v| Description::new().text(...))` — to describe mismatches dynamically.
15///
16/// # Example
17/// ```
18/// # use googletest::prelude::*;
19/// # use googletest_json_serde::json;
20/// # use serde_json::json as j;
21///
22/// let matcher = json::predicate(|v| v.as_i64().map_or(false, |n| n > 0))
23///     .with_description("a positive number", "a non-positive number");
24///
25/// verify_that!(j!(42), &matcher);
26/// verify_that!(j!(-1), not(&matcher));
27/// ```
28///
29/// Use this when no built-in matcher (like `is_string()` or `is_null()`) fits your case.
30pub fn predicate<P>(
31    predicate: P,
32) -> JsonPredicateMatcher<
33    P,
34    __internal_unstable_do_not_depend_on_these::NoDescription,
35    __internal_unstable_do_not_depend_on_these::NoDescription,
36>
37where
38    P: Fn(&Value) -> bool,
39{
40    JsonPredicateMatcher::new(
41        predicate,
42        __internal_unstable_do_not_depend_on_these::NoDescription,
43        __internal_unstable_do_not_depend_on_these::NoDescription,
44    )
45}
46/// Matches any JSON value except null.
47pub fn is_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
48    JsonPredicateMatcher::new(|v| v.is_null(), "JSON null", "which is not JSON null")
49        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
50}
51/// Matches any JSON value except null.
52pub fn is_not_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
53    JsonPredicateMatcher::new(|v| !v.is_null(), "not JSON null", "which is JSON null")
54        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
55}
56
57/// Matches any JSON value except null.
58#[deprecated(since = "0.2.2", note = "Use `is_not_null` instead")]
59pub fn any_value() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
60    JsonPredicateMatcher::new(|v| !v.is_null(), "any JSON value", "is not any JSON value")
61        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
62}
63
64/// Matches JSON string values.
65pub fn is_string() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
66    JsonPredicateMatcher::new(
67        |v| v.is_string(),
68        "a JSON string",
69        "which is not a JSON string",
70    )
71    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
72}
73
74/// Matches JSON number values.
75pub fn is_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
76    JsonPredicateMatcher::new(
77        |v| v.is_number(),
78        "a JSON number",
79        "which is not a JSON number",
80    )
81    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
82}
83
84/// Matches JSON boolean values.
85pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
86    JsonPredicateMatcher::new(
87        |v| v.is_boolean(),
88        "a JSON boolean",
89        "which is not a JSON boolean",
90    )
91    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
92}
93
94/// Matches JSON array values.
95pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
96    JsonPredicateMatcher::new(
97        |v| v.is_array(),
98        "a JSON array",
99        "which is not a JSON array",
100    )
101    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
102}
103
104/// Matches an empty JSON array (`[]`).
105pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
106{
107    JsonPredicateMatcher::new(
108        |v| v.as_array().is_some_and(|a| a.is_empty()),
109        "an empty JSON array",
110        "which is not an empty JSON array",
111    )
112    .with_explain_fn(|v| {
113        if v.is_array() {
114            Description::new().text("which is a non-empty JSON array")
115        } else {
116            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
117        }
118    })
119}
120
121/// Matches JSON object values.
122pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
123    JsonPredicateMatcher::new(
124        |v| v.is_object(),
125        "a JSON object",
126        "which is not a JSON object",
127    )
128    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
129}
130
131/// Matches an empty JSON object (`{}`).
132pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
133{
134    JsonPredicateMatcher::new(
135        |v| v.as_object().is_some_and(|o| o.is_empty()),
136        "an empty JSON object",
137        "which is not an empty JSON object",
138    )
139    .with_explain_fn(|v| {
140        if v.is_object() {
141            Description::new().text("which is a non-empty JSON object")
142        } else {
143            __internal_unstable_do_not_depend_on_these::describe_json_type(v)
144        }
145    })
146}
147
148// Path-based matchers live in `path_matcher.rs`.
149
150#[doc(hidden)]
151pub mod internal {
152    use googletest::description::Description;
153    use googletest::matcher::MatcherResult::{Match, NoMatch};
154    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
155    use serde_json::Value;
156
157    /// Trait for types that can provide a description string.
158    pub trait PredicateDescription {
159        fn to_description(self) -> String;
160    }
161
162    impl PredicateDescription for &'static str {
163        fn to_description(self) -> String {
164            self.to_string()
165        }
166    }
167
168    impl PredicateDescription for String {
169        fn to_description(self) -> String {
170            self
171        }
172    }
173
174    impl<F> PredicateDescription for F
175    where
176        F: Fn() -> String,
177    {
178        fn to_description(self) -> String {
179            self()
180        }
181    }
182    /// Sentinel type for missing descriptions.
183    #[derive(Clone, Copy, Debug)]
184    pub struct NoDescription;
185    impl PredicateDescription for NoDescription {
186        fn to_description(self) -> String {
187            String::new()
188        }
189    }
190
191    /// Type alias for the explain function to reduce type complexity.
192    type ExplainFn = Box<dyn Fn(&Value) -> Description>;
193
194    #[derive(MatcherBase)]
195    pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
196    where
197        P: Fn(&Value) -> bool,
198        D1: PredicateDescription,
199        D2: PredicateDescription,
200    {
201        predicate: P,
202        positive_description: D1,
203        negative_description: D2,
204        explain_fn: Option<ExplainFn>,
205    }
206
207    impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
208    where
209        P: Fn(&Value) -> bool,
210        D1: PredicateDescription,
211        D2: PredicateDescription,
212    {
213        pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
214            Self {
215                predicate,
216                positive_description,
217                negative_description,
218                explain_fn: None,
219            }
220        }
221
222        pub fn with_description<D1b, D2b>(
223            self,
224            positive_description: D1b,
225            negative_description: D2b,
226        ) -> JsonPredicateMatcher<P, D1b, D2b>
227        where
228            D1b: PredicateDescription,
229            D2b: PredicateDescription,
230        {
231            JsonPredicateMatcher {
232                predicate: self.predicate,
233                positive_description,
234                negative_description,
235                explain_fn: self.explain_fn,
236            }
237        }
238
239        pub fn with_explain_fn<F>(mut self, f: F) -> Self
240        where
241            F: Fn(&Value) -> Description + 'static,
242        {
243            self.explain_fn = Some(Box::new(f));
244            self
245        }
246    }
247
248    impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
249    where
250        P: Fn(&Value) -> bool,
251        D1: PredicateDescription + Clone,
252        D2: PredicateDescription + Clone,
253    {
254        fn matches(&self, actual: &Value) -> MatcherResult {
255            if (self.predicate)(actual) {
256                Match
257            } else {
258                NoMatch
259            }
260        }
261
262        fn describe(&self, result: MatcherResult) -> Description {
263            let pos = self.positive_description.clone().to_description();
264            let neg = self.negative_description.clone().to_description();
265
266            match result {
267                Match if pos.is_empty() => "matches predicate".into(),
268                NoMatch if neg.is_empty() => "does not match predicate".into(),
269                Match => pos.into(),
270                NoMatch => neg.into(),
271            }
272        }
273
274        fn explain_match(&self, actual: &Value) -> Description {
275            if let Some(ref f) = self.explain_fn {
276                return f(actual);
277            }
278            Description::new().text("which does not match the predicate")
279        }
280    }
281    /// Marker trait for JSON-aware matchers.
282    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
283        /// Returns true if this matcher allows the field to be missing in an object.
284        fn allows_missing(&self) -> bool {
285            false
286        }
287    }
288
289    /// Trait for converting into a boxed JSON matcher.
290    pub trait IntoJsonMatcher<T> {
291        fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
292    }
293
294    impl<J> IntoJsonMatcher<()> for J
295    where
296        J: JsonMatcher + 'static,
297    {
298        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
299            Box::new(self)
300        }
301    }
302
303    // A concrete matcher that checks equality with an owned serde_json::Value.
304    // This avoids lifetime issues of using googletest::eq on &Value and gives
305    // us control over descriptions.
306    #[derive(googletest::matcher::MatcherBase)]
307    struct JsonEqMatcher {
308        expected: Value,
309    }
310
311    impl Matcher<&Value> for JsonEqMatcher {
312        fn matches(&self, actual: &Value) -> MatcherResult {
313            if *actual == self.expected {
314                Match
315            } else {
316                NoMatch
317            }
318        }
319
320        fn describe(&self, result: MatcherResult) -> Description {
321            match result {
322                Match => format!("is equal to {:?}", self.expected).into(),
323                NoMatch => format!("isn't equal to {:?}", self.expected).into(),
324            }
325        }
326
327        fn explain_match(&self, _actual: &Value) -> Description {
328            // Framework prints the actual value already. Provide the expected.
329            format!("which isn't equal to {:?}", self.expected).into()
330        }
331    }
332
333    impl JsonMatcher for JsonEqMatcher {}
334
335    // Allow &serde_json::Value to be used seamlessly with JSON macros
336    impl IntoJsonMatcher<Value> for &Value {
337        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
338            Box::new(JsonEqMatcher {
339                expected: self.clone(),
340            })
341        }
342    }
343
344    impl IntoJsonMatcher<Value> for Value {
345        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
346            Box::new(JsonEqMatcher { expected: self })
347        }
348    }
349
350    // Literal support marker type
351    pub struct Literal;
352
353    impl IntoJsonMatcher<Literal> for &str {
354        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
355            Box::new(JsonEqMatcher {
356                expected: Value::from(self),
357            })
358        }
359    }
360
361    impl IntoJsonMatcher<Literal> for String {
362        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
363            Box::new(JsonEqMatcher {
364                expected: Value::from(self),
365            })
366        }
367    }
368
369    impl IntoJsonMatcher<Literal> for bool {
370        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
371            Box::new(JsonEqMatcher {
372                expected: Value::from(self),
373            })
374        }
375    }
376
377    impl IntoJsonMatcher<Literal> for i64 {
378        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
379            Box::new(JsonEqMatcher {
380                expected: Value::from(self),
381            })
382        }
383    }
384    impl IntoJsonMatcher<Literal> for i32 {
385        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
386            Box::new(JsonEqMatcher {
387                expected: Value::from(self),
388            })
389        }
390    }
391
392    impl IntoJsonMatcher<Literal> for u64 {
393        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
394            Box::new(JsonEqMatcher {
395                expected: Value::from(self),
396            })
397        }
398    }
399
400    impl IntoJsonMatcher<Literal> for f64 {
401        fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
402            Box::new(JsonEqMatcher {
403                expected: Value::from(self),
404            })
405        }
406    }
407
408    impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
409    where
410        P: Fn(&Value) -> bool + 'static,
411        D1: PredicateDescription + Clone + 'static,
412        D2: PredicateDescription + Clone + 'static,
413    {
414    }
415
416    pub fn describe_json_type(v: &Value) -> Description {
417        match v {
418            Value::Null => "which is a JSON null",
419            Value::String(_) => "which is a JSON string",
420            Value::Number(_) => "which is a JSON number",
421            Value::Bool(_) => "which is a JSON boolean",
422            Value::Array(_) => "which is a JSON array",
423            Value::Object(_) => "which is a JSON object",
424        }
425        .into()
426    }
427}