googletest_json_serde/matchers/
json_matcher.rs1use 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
8pub 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}
46pub 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}
51pub 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#[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
64pub 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
74pub 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
84pub 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
94pub 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
104pub 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
121pub 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 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 #[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 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 pub trait JsonMatcher: for<'a> Matcher<&'a Value> {}
264
265 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 impl<P, D1, D2> IntoJsonMatcher<()> for JsonPredicateMatcher<P, D1, D2>
280 where
281 P: Fn(&Value) -> bool + 'static,
282 D1: PredicateDescription + Clone + 'static,
283 D2: PredicateDescription + Clone + 'static,
284 {
285 fn into_json_matcher(self) -> Box<dyn for<'a> Matcher<&'a Value>> {
286 Box::new(self)
287 }
288 }
289
290 pub fn describe_json_type(v: &Value) -> Description {
291 match v {
292 Value::Null => "which is a JSON null",
293 Value::String(_) => "which is a JSON string",
294 Value::Number(_) => "which is a JSON number",
295 Value::Bool(_) => "which is a JSON boolean",
296 Value::Array(_) => "which is a JSON array",
297 Value::Object(_) => "which is a JSON object",
298 }
299 .into()
300 }
301}