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 serde_json::Value;
6
7/// Creates a custom JSON matcher from an arbitrary predicate function.
8///
9/// This function allows defining ad-hoc JSON matchers inline by supplying a closure or function
10/// that returns `true` for matching values.
11/// The resulting matcher can optionally be extended with:
12/// - `.with_description("expected", "not expected")` — to provide custom messages, and
13/// - `.with_explain_fn(|v| Description::new().text(...))` — to describe mismatches dynamically.
14///
15/// # Example
16/// ```
17/// # use googletest::prelude::*;
18/// # use googletest_json_serde::json;
19/// # use serde_json::json as j;
20///
21/// let matcher = json::predicate(|v| v.as_i64().map_or(false, |n| n > 0))
22///     .with_description("a positive number", "a non-positive number");
23///
24/// verify_that!(j!(42), &matcher);
25/// verify_that!(j!(-1), not(&matcher));
26/// ```
27///
28/// Use this when no built-in matcher (like `is_string()` or `is_null()`) fits your case.
29pub fn predicate<P>(
30    predicate: P,
31) -> JsonPredicateMatcher<
32    P,
33    __internal_unstable_do_not_depend_on_these::NoDescription,
34    __internal_unstable_do_not_depend_on_these::NoDescription,
35>
36where
37    P: Fn(&Value) -> bool,
38{
39    JsonPredicateMatcher::new(
40        predicate,
41        __internal_unstable_do_not_depend_on_these::NoDescription,
42        __internal_unstable_do_not_depend_on_these::NoDescription,
43    )
44}
45/// Matches any JSON value except null.
46pub fn is_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
47    JsonPredicateMatcher::new(|v| v.is_null(), "JSON null", "which is not JSON null")
48        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
49}
50/// Matches any JSON value except null.
51pub fn is_not_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
52    JsonPredicateMatcher::new(|v| !v.is_null(), "not JSON null", "which is JSON null")
53        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
54}
55
56/// Matches any JSON value except null.
57#[deprecated(since = "0.2.2", note = "Use `is_not_null` instead")]
58pub fn any_value() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
59    JsonPredicateMatcher::new(|v| !v.is_null(), "any JSON value", "is not any JSON value")
60        .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
61}
62
63/// Matches JSON string values.
64pub fn is_string() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
65    JsonPredicateMatcher::new(
66        |v| v.is_string(),
67        "a JSON string",
68        "which is not a JSON string",
69    )
70    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
71}
72
73/// Matches JSON number values.
74pub fn is_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
75    JsonPredicateMatcher::new(
76        |v| v.is_number(),
77        "a JSON number",
78        "which is not a JSON number",
79    )
80    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
81}
82
83/// Matches JSON boolean values.
84pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
85    JsonPredicateMatcher::new(
86        |v| v.is_boolean(),
87        "a JSON boolean",
88        "which is not a JSON boolean",
89    )
90    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
91}
92
93/// Matches JSON array values.
94pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
95    JsonPredicateMatcher::new(
96        |v| v.is_array(),
97        "a JSON array",
98        "which is not a JSON array",
99    )
100    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
101}
102
103/// Matches JSON object values.
104pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
105    JsonPredicateMatcher::new(
106        |v| v.is_object(),
107        "a JSON object",
108        "which is not a JSON object",
109    )
110    .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
111}
112
113#[doc(hidden)]
114pub mod internal {
115    use googletest::description::Description;
116    use googletest::matcher::MatcherResult::{Match, NoMatch};
117    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
118    use serde_json::Value;
119
120    /// Trait for types that can provide a description string.
121    pub trait PredicateDescription {
122        fn to_description(self) -> String;
123    }
124
125    impl PredicateDescription for &'static str {
126        fn to_description(self) -> String {
127            self.to_string()
128        }
129    }
130
131    impl PredicateDescription for String {
132        fn to_description(self) -> String {
133            self
134        }
135    }
136
137    impl<F> PredicateDescription for F
138    where
139        F: Fn() -> String,
140    {
141        fn to_description(self) -> String {
142            self()
143        }
144    }
145    /// Sentinel type for missing descriptions.
146    #[derive(Clone, Copy, Debug)]
147    pub struct NoDescription;
148    impl PredicateDescription for NoDescription {
149        fn to_description(self) -> String {
150            String::new()
151        }
152    }
153
154    /// Type alias for the explain function to reduce type complexity.
155    type ExplainFn = Box<dyn Fn(&Value) -> Description>;
156
157    #[derive(MatcherBase)]
158    pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
159    where
160        P: Fn(&Value) -> bool,
161        D1: PredicateDescription,
162        D2: PredicateDescription,
163    {
164        predicate: P,
165        positive_description: D1,
166        negative_description: D2,
167        explain_fn: Option<ExplainFn>,
168    }
169
170    impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
171    where
172        P: Fn(&Value) -> bool,
173        D1: PredicateDescription,
174        D2: PredicateDescription,
175    {
176        pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
177            Self {
178                predicate,
179                positive_description,
180                negative_description,
181                explain_fn: None,
182            }
183        }
184
185        pub fn with_description<D1b, D2b>(
186            self,
187            positive_description: D1b,
188            negative_description: D2b,
189        ) -> JsonPredicateMatcher<P, D1b, D2b>
190        where
191            D1b: PredicateDescription,
192            D2b: PredicateDescription,
193        {
194            JsonPredicateMatcher {
195                predicate: self.predicate,
196                positive_description,
197                negative_description,
198                explain_fn: self.explain_fn,
199            }
200        }
201
202        pub fn with_explain_fn<F>(mut self, f: F) -> Self
203        where
204            F: Fn(&Value) -> Description + 'static,
205        {
206            self.explain_fn = Some(Box::new(f));
207            self
208        }
209    }
210
211    impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
212    where
213        P: Fn(&Value) -> bool,
214        D1: PredicateDescription + Clone,
215        D2: PredicateDescription + Clone,
216    {
217        fn matches(&self, actual: &Value) -> MatcherResult {
218            if (self.predicate)(actual) {
219                Match
220            } else {
221                NoMatch
222            }
223        }
224
225        fn describe(&self, result: MatcherResult) -> Description {
226            let pos = self.positive_description.clone().to_description();
227            let neg = self.negative_description.clone().to_description();
228
229            match result {
230                Match if pos.is_empty() => "matches predicate".into(),
231                NoMatch if neg.is_empty() => "does not match predicate".into(),
232                Match => pos.into(),
233                NoMatch => neg.into(),
234            }
235        }
236
237        fn explain_match(&self, actual: &Value) -> Description {
238            if let Some(ref f) = self.explain_fn {
239                return f(actual);
240            }
241            Description::new().text("which does not match the predicate")
242        }
243    }
244    /// Marker trait for JSON-aware matchers.
245    pub trait JsonMatcher: for<'a> Matcher<&'a Value> {}
246
247    /// Trait for converting into a boxed JSON matcher.
248    pub trait IntoJsonMatcher<T> {
249        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>>;
250    }
251
252    impl<J> IntoJsonMatcher<()> for J
253    where
254        J: JsonMatcher + 'static,
255    {
256        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
257            Box::new(self)
258        }
259    }
260
261    impl<P, D1, D2> IntoJsonMatcher<()> for JsonPredicateMatcher<P, D1, D2>
262    where
263        P: Fn(&Value) -> bool + 'static,
264        D1: PredicateDescription + Clone + 'static,
265        D2: PredicateDescription + Clone + 'static,
266    {
267        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
268            Box::new(self)
269        }
270    }
271
272    pub fn describe_json_type(v: &Value) -> Description {
273        match v {
274            Value::Null => "which is a JSON null",
275            Value::String(_) => "which is a JSON string",
276            Value::Number(_) => "which is a JSON number",
277            Value::Bool(_) => "which is a JSON boolean",
278            Value::Array(_) => "which is a JSON array",
279            Value::Object(_) => "which is a JSON object",
280        }
281        .into()
282    }
283}