googletest_json_serde/matchers/
value_matcher.rs

1//! Utility matchers and macros for concise JSON assertions using googletest.
2
3/// Matches a JSON value (string, number, or boolean) against the given matcher.
4///
5/// This macro enables matching specific primitive values inside a JSON structure
6/// by delegating to a matcher for the corresponding Rust type. It supports:
7/// - `String` values (e.g. `json::value!(eq("hello"))`)
8/// - `Number` values as `i64` or `f64` (e.g. `json::value!(ge(0))`)
9/// - `Boolean` values (e.g. `json::value!(eq(true))`)
10///
11/// Fails if the value is not of the expected JSON type.
12///
13/// # Example
14/// ```
15/// # use googletest::prelude::*;
16/// # use googletest_json_serde::json;
17/// # use serde_json::json as j;
18/// let data = j!({"active": true, "count": 3});
19///
20/// verify_that!(data["active"], json::value!(eq(true)));
21/// verify_that!(data["count"], json::value!(ge(0)));
22/// ```
23#[macro_export]
24#[doc(hidden)]
25macro_rules! __json_value {
26    ($matcher:expr) => {
27        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonValueMatcher::new(
28            $matcher,
29        )
30    };
31}
32
33pub fn is_null() -> crate::matchers::__internal_unstable_do_not_depend_on_these::IsJsonNull {
34    crate::matchers::__internal_unstable_do_not_depend_on_these::IsJsonNull
35}
36
37#[doc(hidden)]
38pub mod internal {
39    use googletest::description::Description;
40    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
41    use serde_json::Value;
42
43    #[doc(hidden)]
44    #[derive(MatcherBase)]
45    pub struct JsonValueMatcher<M, T> {
46        inner: M,
47        phantom: std::marker::PhantomData<T>,
48    }
49
50    impl<M, T> JsonValueMatcher<M, T> {
51        pub fn new(inner: M) -> Self {
52            Self {
53                inner,
54                phantom: std::marker::PhantomData,
55            }
56        }
57    }
58
59    impl<M> Matcher<&Value> for JsonValueMatcher<M, String>
60    where
61        M: for<'a> Matcher<&'a str>,
62    {
63        fn matches(&self, actual: &Value) -> MatcherResult {
64            match actual {
65                Value::String(s) => self.inner.matches(s),
66                _ => MatcherResult::NoMatch,
67            }
68        }
69        fn describe(&self, r: MatcherResult) -> Description {
70            self.inner.describe(r)
71        }
72        fn explain_match(&self, actual: &Value) -> Description {
73            match actual {
74                Value::String(s) => self.inner.explain_match(s),
75                _ => Description::new().text("which is not a JSON string".to_string()),
76            }
77        }
78    }
79
80    impl<M> Matcher<&Value> for JsonValueMatcher<M, i64>
81    where
82        M: Matcher<i64>,
83    {
84        fn matches(&self, actual: &Value) -> MatcherResult {
85            match actual {
86                Value::Number(n) => n
87                    .as_i64()
88                    .map_or(MatcherResult::NoMatch, |i| self.inner.matches(i)),
89                _ => MatcherResult::NoMatch,
90            }
91        }
92        fn describe(&self, r: MatcherResult) -> Description {
93            self.inner.describe(r)
94        }
95        fn explain_match(&self, actual: &Value) -> Description {
96            match actual {
97                Value::Number(n) => match n.as_i64() {
98                    Some(i) => self.inner.explain_match(i),
99                    None => Description::new().text(format!("number out of i64 range: {n}")),
100                },
101                _ => Description::new().text("which is not a JSON number"),
102            }
103        }
104    }
105
106    impl<M> Matcher<&Value> for JsonValueMatcher<M, f64>
107    where
108        M: Matcher<f64>,
109    {
110        fn matches(&self, actual: &Value) -> MatcherResult {
111            match actual {
112                Value::Number(n) => n
113                    .as_f64()
114                    .map_or(MatcherResult::NoMatch, |f| self.inner.matches(f)),
115                _ => MatcherResult::NoMatch,
116            }
117        }
118        fn describe(&self, r: MatcherResult) -> Description {
119            self.inner.describe(r)
120        }
121        fn explain_match(&self, actual: &Value) -> Description {
122            match actual {
123                Value::Number(n) => match n.as_f64() {
124                    Some(f) => self.inner.explain_match(f),
125                    None => Description::new().text(format!("number not convertible to f64: {n}")),
126                },
127                _ => Description::new().text("which is not a JSON number"),
128            }
129        }
130    }
131
132    impl<M> Matcher<&Value> for JsonValueMatcher<M, bool>
133    where
134        M: Matcher<bool>,
135    {
136        fn matches(&self, actual: &Value) -> MatcherResult {
137            match actual {
138                Value::Bool(b) => self.inner.matches(*b),
139                _ => MatcherResult::NoMatch,
140            }
141        }
142        fn describe(&self, r: MatcherResult) -> Description {
143            self.inner.describe(r)
144        }
145        fn explain_match(&self, actual: &Value) -> Description {
146            match actual {
147                Value::Bool(b) => self.inner.explain_match(*b),
148                _ => Description::new().text("which is not a JSON boolean"),
149            }
150        }
151    }
152
153    #[derive(MatcherBase)]
154    pub struct IsJsonNull;
155    impl Matcher<&Value> for IsJsonNull {
156        fn matches(&self, actual: &Value) -> MatcherResult {
157            match actual {
158                Value::Null => MatcherResult::Match,
159                _ => MatcherResult::NoMatch,
160            }
161        }
162
163        fn describe(&self, _: MatcherResult) -> Description {
164            Description::new().text("JSON null")
165        }
166
167        fn explain_match(&self, actual: &Value) -> Description {
168            match actual {
169                Value::Null => Description::new().text("which is null"),
170                _ => Description::new().text("which is not JSON null"),
171            }
172        }
173    }
174}