googletest_json_serde/matchers/
primitive_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#[deprecated(since = "0.2.0", note = "please use `json::primitive!` instead")]
24#[macro_export]
25#[doc(hidden)]
26macro_rules! __json_value {
27    ($matcher:expr) => {
28        $crate::__json_primitive!($matcher)
29    };
30}
31
32/// Matches a JSON value (string, number, or boolean) against the given matcher.
33///
34/// This macro enables matching specific primitive values inside a JSON structure
35/// by delegating to a matcher for the corresponding Rust type. It supports:
36/// - `String` values (e.g. `json::primitive!(eq("hello"))`)
37/// - `Number` values as `i64` or `f64` (e.g. `json::primitive!(ge(0))`)
38/// - `Boolean` values (e.g. `json::primitive!(eq(true))`)
39///
40/// Fails if the value is not of the expected JSON type.
41///
42/// # Example
43/// ```
44/// # use googletest::prelude::*;
45/// # use googletest_json_serde::json;
46/// # use serde_json::json as j;
47/// let data = j!({"active": true, "count": 3});
48///
49/// verify_that!(data["active"], json::primitive!(eq(true)));
50/// verify_that!(data["count"], json::primitive!(ge(0)));
51/// ```
52#[macro_export]
53#[doc(hidden)]
54macro_rules! __json_primitive {
55    ($matcher:expr) => {
56        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPrimitiveMatcher::new(
57            $matcher,
58        )
59    };
60}
61
62#[doc(hidden)]
63pub mod internal {
64    use crate::matchers::json_matcher::internal::{IntoJsonMatcher, JsonMatcher};
65    use googletest::description::Description;
66    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
67    use serde_json::Value;
68
69    #[doc(hidden)]
70    #[derive(MatcherBase)]
71    pub struct JsonPrimitiveMatcher<M, T> {
72        inner: M,
73        phantom: std::marker::PhantomData<T>,
74    }
75
76    impl<M, T> JsonPrimitiveMatcher<M, T> {
77        pub fn new(inner: M) -> Self {
78            Self {
79                inner,
80                phantom: std::marker::PhantomData,
81            }
82        }
83    }
84
85    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, String>
86    where
87        M: for<'a> Matcher<&'a str>,
88    {
89        fn matches(&self, actual: &Value) -> MatcherResult {
90            match actual {
91                Value::String(s) => self.inner.matches(s),
92                _ => MatcherResult::NoMatch,
93            }
94        }
95        fn describe(&self, r: MatcherResult) -> Description {
96            self.inner.describe(r)
97        }
98        fn explain_match(&self, actual: &Value) -> Description {
99            match actual {
100                Value::String(s) => self.inner.explain_match(s),
101                _ => Description::new().text("which is not a JSON string".to_string()),
102            }
103        }
104    }
105
106    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i64>
107    where
108        M: Matcher<i64>,
109    {
110        fn matches(&self, actual: &Value) -> MatcherResult {
111            match actual {
112                Value::Number(n) => n
113                    .as_i64()
114                    .map_or(MatcherResult::NoMatch, |i| self.inner.matches(i)),
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_i64() {
124                    Some(i) => self.inner.explain_match(i),
125                    None => Description::new().text(format!("number out of i64 range: {n}")),
126                },
127                _ => Description::new().text("which is not a JSON number"),
128            }
129        }
130    }
131
132    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, f64>
133    where
134        M: Matcher<f64>,
135    {
136        fn matches(&self, actual: &Value) -> MatcherResult {
137            match actual {
138                Value::Number(n) => n
139                    .as_f64()
140                    .map_or(MatcherResult::NoMatch, |f| self.inner.matches(f)),
141                _ => MatcherResult::NoMatch,
142            }
143        }
144        fn describe(&self, r: MatcherResult) -> Description {
145            self.inner.describe(r)
146        }
147        fn explain_match(&self, actual: &Value) -> Description {
148            match actual {
149                Value::Number(n) => match n.as_f64() {
150                    Some(f) => self.inner.explain_match(f),
151                    None => Description::new().text(format!("number not convertible to f64: {n}")),
152                },
153                _ => Description::new().text("which is not a JSON number"),
154            }
155        }
156    }
157
158    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, bool>
159    where
160        M: Matcher<bool>,
161    {
162        fn matches(&self, actual: &Value) -> MatcherResult {
163            match actual {
164                Value::Bool(b) => self.inner.matches(*b),
165                _ => MatcherResult::NoMatch,
166            }
167        }
168        fn describe(&self, r: MatcherResult) -> Description {
169            self.inner.describe(r)
170        }
171        fn explain_match(&self, actual: &Value) -> Description {
172            match actual {
173                Value::Bool(b) => self.inner.explain_match(*b),
174                _ => Description::new().text("which is not a JSON boolean"),
175            }
176        }
177    }
178
179    impl<M, T> JsonMatcher for JsonPrimitiveMatcher<M, T> where
180        JsonPrimitiveMatcher<M, T>: for<'a> Matcher<&'a Value>
181    {
182    }
183
184    /// Trait for converting into a boxed JSON matcher.
185    impl<M> IntoJsonMatcher<i64> for M
186    where
187        M: Matcher<i64> + 'static,
188    {
189        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
190            Box::new(JsonPrimitiveMatcher::<M, i64>::new(self))
191        }
192    }
193
194    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, u64>
195    where
196        M: Matcher<u64>,
197    {
198        fn matches(&self, actual: &Value) -> MatcherResult {
199            match actual {
200                Value::Number(n) => n
201                    .as_u64()
202                    .map_or(MatcherResult::NoMatch, |u| self.inner.matches(u)),
203                _ => MatcherResult::NoMatch,
204            }
205        }
206        fn describe(&self, r: MatcherResult) -> Description {
207            self.inner.describe(r)
208        }
209        fn explain_match(&self, actual: &Value) -> Description {
210            match actual {
211                Value::Number(n) => match n.as_u64() {
212                    Some(u) => self.inner.explain_match(u),
213                    None => Description::new().text(format!("number out of u64 range: {n}")),
214                },
215                _ => Description::new().text("which is not a JSON number"),
216            }
217        }
218    }
219
220    impl<M> IntoJsonMatcher<u64> for M
221    where
222        M: Matcher<u64> + 'static,
223    {
224        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
225            Box::new(JsonPrimitiveMatcher::<M, u64>::new(self))
226        }
227    }
228
229    impl<M> IntoJsonMatcher<f64> for M
230    where
231        M: Matcher<f64> + 'static,
232    {
233        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
234            Box::new(JsonPrimitiveMatcher::<M, f64>::new(self))
235        }
236    }
237
238    impl<M> IntoJsonMatcher<String> for M
239    where
240        M: for<'a> Matcher<&'a str> + 'static,
241    {
242        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
243            Box::new(JsonPrimitiveMatcher::<M, String>::new(self))
244        }
245    }
246
247    impl<M> IntoJsonMatcher<bool> for M
248    where
249        M: Matcher<bool> + 'static,
250    {
251        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
252            Box::new(JsonPrimitiveMatcher::<M, bool>::new(self))
253        }
254    }
255
256    impl<M> Matcher<&Value> for JsonPrimitiveMatcher<M, i32>
257    where
258        M: Matcher<i32>,
259    {
260        fn matches(&self, actual: &Value) -> MatcherResult {
261            match actual {
262                Value::Number(n) => match n.as_i64() {
263                    Some(i) => match i32::try_from(i) {
264                        Ok(i32_val) => self.inner.matches(i32_val),
265                        Err(_) => MatcherResult::NoMatch,
266                    },
267                    None => MatcherResult::NoMatch,
268                },
269                _ => MatcherResult::NoMatch,
270            }
271        }
272        fn describe(&self, r: MatcherResult) -> Description {
273            self.inner.describe(r)
274        }
275        fn explain_match(&self, actual: &Value) -> Description {
276            match actual {
277                Value::Number(n) => match n.as_i64() {
278                    Some(i) => match i32::try_from(i) {
279                        Ok(i32_val) => self.inner.explain_match(i32_val),
280                        Err(_) => Description::new().text(format!("number out of i32 range: {n}")),
281                    },
282                    None => Description::new().text(format!("number out of i64 range: {n}")),
283                },
284                _ => Description::new().text("which is not a JSON number"),
285            }
286        }
287    }
288
289    impl<M> IntoJsonMatcher<i32> for M
290    where
291        M: Matcher<i32> + 'static,
292    {
293        fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
294            Box::new(JsonPrimitiveMatcher::<M, i32>::new(self))
295        }
296    }
297}