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#[doc(hidden)]
132pub mod internal {
133    use googletest::description::Description;
134    use googletest::matcher::MatcherResult::{Match, NoMatch};
135    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
136    use serde_json::Value;
137
138    /// Trait for types that can provide a description string.
139    pub trait PredicateDescription {
140        fn to_description(self) -> String;
141    }
142
143    impl PredicateDescription for &'static str {
144        fn to_description(self) -> String {
145            self.to_string()
146        }
147    }
148
149    impl PredicateDescription for String {
150        fn to_description(self) -> String {
151            self
152        }
153    }
154
155    impl<F> PredicateDescription for F
156    where
157        F: Fn() -> String,
158    {
159        fn to_description(self) -> String {
160            self()
161        }
162    }
163    /// Sentinel type for missing descriptions.
164    #[derive(Clone, Copy, Debug)]
165    pub struct NoDescription;
166    impl PredicateDescription for NoDescription {
167        fn to_description(self) -> String {
168            String::new()
169        }
170    }
171
172    /// Type alias for the explain function to reduce type complexity.
173    type ExplainFn = Box<dyn Fn(&Value) -> Description>;
174
175    #[derive(MatcherBase)]
176    pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
177    where
178        P: Fn(&Value) -> bool,
179        D1: PredicateDescription,
180        D2: PredicateDescription,
181    {
182        predicate: P,
183        positive_description: D1,
184        negative_description: D2,
185        explain_fn: Option<ExplainFn>,
186    }
187
188    impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
189    where
190        P: Fn(&Value) -> bool,
191        D1: PredicateDescription,
192        D2: PredicateDescription,
193    {
194        pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
195            Self {
196                predicate,
197                positive_description,
198                negative_description,
199                explain_fn: None,
200            }
201        }
202
203        pub fn with_description<D1b, D2b>(
204            self,
205            positive_description: D1b,
206            negative_description: D2b,
207        ) -> JsonPredicateMatcher<P, D1b, D2b>
208        where
209            D1b: PredicateDescription,
210            D2b: PredicateDescription,
211        {
212            JsonPredicateMatcher {
213                predicate: self.predicate,
214                positive_description,
215                negative_description,
216                explain_fn: self.explain_fn,
217            }
218        }
219
220        pub fn with_explain_fn<F>(mut self, f: F) -> Self
221        where
222            F: Fn(&Value) -> Description + 'static,
223        {
224            self.explain_fn = Some(Box::new(f));
225            self
226        }
227    }
228
229    impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
230    where
231        P: Fn(&Value) -> bool,
232        D1: PredicateDescription + Clone,
233        D2: PredicateDescription + Clone,
234    {
235        fn matches(&self, actual: &Value) -> MatcherResult {
236            if (self.predicate)(actual) {
237                Match
238            } else {
239                NoMatch
240            }
241        }
242
243        fn describe(&self, result: MatcherResult) -> Description {
244            let pos = self.positive_description.clone().to_description();
245            let neg = self.negative_description.clone().to_description();
246
247            match result {
248                Match if pos.is_empty() => "matches predicate".into(),
249                NoMatch if neg.is_empty() => "does not match predicate".into(),
250                Match => pos.into(),
251                NoMatch => neg.into(),
252            }
253        }
254
255        fn explain_match(&self, actual: &Value) -> Description {
256            if let Some(ref f) = self.explain_fn {
257                return f(actual);
258            }
259            Description::new().text("which does not match the predicate")
260        }
261    }
262    /// Marker trait for JSON-aware matchers.
263    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {}
264
265    /// Trait for converting into a boxed JSON matcher.
266    pub trait IntoJsonMatcher<T> {
267        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>>;
268    }
269
270    impl<J> IntoJsonMatcher<()> for J
271    where
272        J: JsonMatcher + 'static,
273    {
274        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
275            Box::new(self)
276        }
277    }
278
279    // A concrete matcher that checks equality with an owned serde_json::Value.
280    // This avoids lifetime issues of using googletest::eq on &Value and gives
281    // us control over descriptions.
282    #[derive(googletest::matcher::MatcherBase)]
283    struct JsonEqMatcher {
284        expected: Value,
285    }
286
287    impl Matcher<&Value> for JsonEqMatcher {
288        fn matches(&self, actual: &Value) -> MatcherResult {
289            if *actual == self.expected {
290                Match
291            } else {
292                NoMatch
293            }
294        }
295
296        fn describe(&self, result: MatcherResult) -> Description {
297            match result {
298                Match => format!("is equal to {:?}", self.expected).into(),
299                NoMatch => format!("isn't equal to {:?}", self.expected).into(),
300            }
301        }
302
303        fn explain_match(&self, _actual: &Value) -> Description {
304            // Framework prints the actual value already. Provide the expected.
305            format!("which isn't equal to {:?}", self.expected).into()
306        }
307    }
308
309    // Allow &serde_json::Value to be used seamlessly with JSON macros
310    impl IntoJsonMatcher<Value> for &Value {
311        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
312            Box::new(JsonEqMatcher {
313                expected: self.clone(),
314            })
315        }
316    }
317
318    impl IntoJsonMatcher<Value> for Value {
319        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
320            Box::new(JsonEqMatcher { expected: self })
321        }
322    }
323
324    impl<P, D1, D2> IntoJsonMatcher<()> for JsonPredicateMatcher<P, D1, D2>
325    where
326        P: Fn(&Value) -> bool + 'static,
327        D1: PredicateDescription + Clone + 'static,
328        D2: PredicateDescription + Clone + 'static,
329    {
330        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
331            Box::new(self)
332        }
333    }
334
335    pub fn describe_json_type(v: &Value) -> Description {
336        match v {
337            Value::Null => "which is a JSON null",
338            Value::String(_) => "which is a JSON string",
339            Value::Number(_) => "which is a JSON number",
340            Value::Bool(_) => "which is a JSON boolean",
341            Value::Array(_) => "which is a JSON array",
342            Value::Object(_) => "which is a JSON object",
343        }
344        .into()
345    }
346}