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
131pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
133{
134 JsonPredicateMatcher::new(
135 |v| v.as_object().is_some_and(|o| o.is_empty()),
136 "an empty JSON object",
137 "which is not an empty JSON object",
138 )
139 .with_explain_fn(|v| {
140 if v.is_object() {
141 Description::new().text("which is a non-empty JSON object")
142 } else {
143 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
144 }
145 })
146}
147
148#[doc(hidden)]
151pub mod internal {
152 use googletest::description::Description;
153 use googletest::matcher::MatcherResult::{Match, NoMatch};
154 use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
155 use serde_json::Value;
156
157 pub trait PredicateDescription {
159 fn to_description(self) -> String;
160 }
161
162 impl PredicateDescription for &'static str {
163 fn to_description(self) -> String {
164 self.to_string()
165 }
166 }
167
168 impl PredicateDescription for String {
169 fn to_description(self) -> String {
170 self
171 }
172 }
173
174 impl<F> PredicateDescription for F
175 where
176 F: Fn() -> String,
177 {
178 fn to_description(self) -> String {
179 self()
180 }
181 }
182 #[derive(Clone, Copy, Debug)]
184 pub struct NoDescription;
185 impl PredicateDescription for NoDescription {
186 fn to_description(self) -> String {
187 String::new()
188 }
189 }
190
191 type ExplainFn = Box<dyn Fn(&Value) -> Description>;
193
194 #[derive(MatcherBase)]
195 pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
196 where
197 P: Fn(&Value) -> bool,
198 D1: PredicateDescription,
199 D2: PredicateDescription,
200 {
201 predicate: P,
202 positive_description: D1,
203 negative_description: D2,
204 explain_fn: Option<ExplainFn>,
205 }
206
207 impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
208 where
209 P: Fn(&Value) -> bool,
210 D1: PredicateDescription,
211 D2: PredicateDescription,
212 {
213 pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
214 Self {
215 predicate,
216 positive_description,
217 negative_description,
218 explain_fn: None,
219 }
220 }
221
222 pub fn with_description<D1b, D2b>(
223 self,
224 positive_description: D1b,
225 negative_description: D2b,
226 ) -> JsonPredicateMatcher<P, D1b, D2b>
227 where
228 D1b: PredicateDescription,
229 D2b: PredicateDescription,
230 {
231 JsonPredicateMatcher {
232 predicate: self.predicate,
233 positive_description,
234 negative_description,
235 explain_fn: self.explain_fn,
236 }
237 }
238
239 pub fn with_explain_fn<F>(mut self, f: F) -> Self
240 where
241 F: Fn(&Value) -> Description + 'static,
242 {
243 self.explain_fn = Some(Box::new(f));
244 self
245 }
246 }
247
248 impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
249 where
250 P: Fn(&Value) -> bool,
251 D1: PredicateDescription + Clone,
252 D2: PredicateDescription + Clone,
253 {
254 fn matches(&self, actual: &Value) -> MatcherResult {
255 if (self.predicate)(actual) {
256 Match
257 } else {
258 NoMatch
259 }
260 }
261
262 fn describe(&self, result: MatcherResult) -> Description {
263 let pos = self.positive_description.clone().to_description();
264 let neg = self.negative_description.clone().to_description();
265
266 match result {
267 Match if pos.is_empty() => "matches predicate".into(),
268 NoMatch if neg.is_empty() => "does not match predicate".into(),
269 Match => pos.into(),
270 NoMatch => neg.into(),
271 }
272 }
273
274 fn explain_match(&self, actual: &Value) -> Description {
275 if let Some(ref f) = self.explain_fn {
276 return f(actual);
277 }
278 Description::new().text("which does not match the predicate")
279 }
280 }
281 pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
283 fn allows_missing(&self) -> bool {
285 false
286 }
287 }
288
289 pub trait IntoJsonMatcher<T> {
291 fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
292 }
293
294 impl<J> IntoJsonMatcher<()> for J
295 where
296 J: JsonMatcher + 'static,
297 {
298 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
299 Box::new(self)
300 }
301 }
302
303 #[derive(googletest::matcher::MatcherBase)]
307 struct JsonEqMatcher {
308 expected: Value,
309 }
310
311 impl Matcher<&Value> for JsonEqMatcher {
312 fn matches(&self, actual: &Value) -> MatcherResult {
313 if *actual == self.expected {
314 Match
315 } else {
316 NoMatch
317 }
318 }
319
320 fn describe(&self, result: MatcherResult) -> Description {
321 match result {
322 Match => format!("is equal to {:?}", self.expected).into(),
323 NoMatch => format!("isn't equal to {:?}", self.expected).into(),
324 }
325 }
326
327 fn explain_match(&self, _actual: &Value) -> Description {
328 format!("which isn't equal to {:?}", self.expected).into()
330 }
331 }
332
333 impl JsonMatcher for JsonEqMatcher {}
334
335 impl IntoJsonMatcher<Value> for &Value {
337 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
338 Box::new(JsonEqMatcher {
339 expected: self.clone(),
340 })
341 }
342 }
343
344 impl IntoJsonMatcher<Value> for Value {
345 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
346 Box::new(JsonEqMatcher { expected: self })
347 }
348 }
349
350 pub struct Literal;
352
353 impl IntoJsonMatcher<Literal> for &str {
354 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
355 Box::new(JsonEqMatcher {
356 expected: Value::from(self),
357 })
358 }
359 }
360
361 impl IntoJsonMatcher<Literal> for String {
362 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
363 Box::new(JsonEqMatcher {
364 expected: Value::from(self),
365 })
366 }
367 }
368
369 impl IntoJsonMatcher<Literal> for bool {
370 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
371 Box::new(JsonEqMatcher {
372 expected: Value::from(self),
373 })
374 }
375 }
376
377 impl IntoJsonMatcher<Literal> for i64 {
378 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
379 Box::new(JsonEqMatcher {
380 expected: Value::from(self),
381 })
382 }
383 }
384 impl IntoJsonMatcher<Literal> for i32 {
385 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
386 Box::new(JsonEqMatcher {
387 expected: Value::from(self),
388 })
389 }
390 }
391
392 impl IntoJsonMatcher<Literal> for u64 {
393 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
394 Box::new(JsonEqMatcher {
395 expected: Value::from(self),
396 })
397 }
398 }
399
400 impl IntoJsonMatcher<Literal> for f64 {
401 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
402 Box::new(JsonEqMatcher {
403 expected: Value::from(self),
404 })
405 }
406 }
407
408 impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
409 where
410 P: Fn(&Value) -> bool + 'static,
411 D1: PredicateDescription + Clone + 'static,
412 D2: PredicateDescription + Clone + 'static,
413 {
414 }
415
416 pub fn describe_json_type(v: &Value) -> Description {
417 match v {
418 Value::Null => "which is a JSON null",
419 Value::String(_) => "which is a JSON string",
420 Value::Number(_) => "which is a JSON number",
421 Value::Bool(_) => "which is a JSON boolean",
422 Value::Array(_) => "which is a JSON array",
423 Value::Object(_) => "which is a JSON object",
424 }
425 .into()
426 }
427}