googletest_json_serde/matchers/
json_matcher.rs1use crate::matchers::__internal_unstable_do_not_depend_on_these;
12use crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPredicateMatcher;
13use googletest::description::Description;
14use serde_json::Value;
15
16pub fn predicate<P>(
29 predicate: P,
30) -> JsonPredicateMatcher<
31 P,
32 __internal_unstable_do_not_depend_on_these::NoDescription,
33 __internal_unstable_do_not_depend_on_these::NoDescription,
34>
35where
36 P: Fn(&Value) -> bool,
37{
38 JsonPredicateMatcher::new(
39 predicate,
40 __internal_unstable_do_not_depend_on_these::NoDescription,
41 __internal_unstable_do_not_depend_on_these::NoDescription,
42 )
43}
44pub fn is_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
56 JsonPredicateMatcher::new(|v| v.is_null(), "JSON null", "which is not JSON null")
57 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
58}
59pub fn is_not_null() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
71 JsonPredicateMatcher::new(|v| !v.is_null(), "not JSON null", "which is JSON null")
72 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
73}
74
75#[deprecated(since = "0.2.2", note = "Use `is_not_null` instead")]
87pub fn any_value() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
88 JsonPredicateMatcher::new(|v| !v.is_null(), "any JSON value", "is not any JSON value")
89 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
90}
91
92pub fn is_string() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
104 JsonPredicateMatcher::new(
105 |v| v.is_string(),
106 "a JSON string",
107 "which is not a JSON string",
108 )
109 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
110}
111
112pub fn is_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
124 JsonPredicateMatcher::new(
125 |v| v.is_number(),
126 "a JSON number",
127 "which is not a JSON number",
128 )
129 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
130}
131
132pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
144 JsonPredicateMatcher::new(
145 |v| v.is_boolean(),
146 "a JSON boolean",
147 "which is not a JSON boolean",
148 )
149 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
150}
151
152pub fn is_true() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
164 JsonPredicateMatcher::new(
165 |v| matches!(v, Value::Bool(true)),
166 "JSON true",
167 "which is not JSON true",
168 )
169 .with_explain_fn(|v| match v {
170 Value::Bool(false) => Description::new().text("which is JSON false"),
171 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
172 })
173}
174
175pub fn is_false() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
187 JsonPredicateMatcher::new(
188 |v| matches!(v, Value::Bool(false)),
189 "JSON false",
190 "which is not JSON false",
191 )
192 .with_explain_fn(|v| match v {
193 Value::Bool(true) => Description::new().text("which is JSON true"),
194 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
195 })
196}
197
198pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
210 JsonPredicateMatcher::new(
211 |v| v.is_array(),
212 "a JSON array",
213 "which is not a JSON array",
214 )
215 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
216}
217
218pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
230{
231 JsonPredicateMatcher::new(
232 |v| v.as_array().is_some_and(|a| a.is_empty()),
233 "an empty JSON array",
234 "which is not an empty JSON array",
235 )
236 .with_explain_fn(|v| {
237 if v.is_array() {
238 Description::new().text("which is a non-empty JSON array")
239 } else {
240 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
241 }
242 })
243}
244
245pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
257 JsonPredicateMatcher::new(
258 |v| v.is_object(),
259 "a JSON object",
260 "which is not a JSON object",
261 )
262 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
263}
264
265pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
277{
278 JsonPredicateMatcher::new(
279 |v| v.as_object().is_some_and(|o| o.is_empty()),
280 "an empty JSON object",
281 "which is not an empty JSON object",
282 )
283 .with_explain_fn(|v| {
284 if v.is_object() {
285 Description::new().text("which is a non-empty JSON object")
286 } else {
287 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
288 }
289 })
290}
291
292#[doc(hidden)]
295pub mod internal {
296 use googletest::description::Description;
297 use googletest::matcher::MatcherResult::{Match, NoMatch};
298 use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
299 use serde_json::Value;
300
301 pub trait PredicateDescription {
303 fn to_description(self) -> String;
304 }
305
306 impl PredicateDescription for &'static str {
307 fn to_description(self) -> String {
308 self.to_string()
309 }
310 }
311
312 impl PredicateDescription for String {
313 fn to_description(self) -> String {
314 self
315 }
316 }
317
318 impl<F> PredicateDescription for F
319 where
320 F: Fn() -> String,
321 {
322 fn to_description(self) -> String {
323 self()
324 }
325 }
326 #[derive(Clone, Copy, Debug)]
328 pub struct NoDescription;
329 impl PredicateDescription for NoDescription {
330 fn to_description(self) -> String {
331 String::new()
332 }
333 }
334
335 type ExplainFn = Box<dyn Fn(&Value) -> Description>;
337
338 #[derive(MatcherBase)]
339 pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
340 where
341 P: Fn(&Value) -> bool,
342 D1: PredicateDescription,
343 D2: PredicateDescription,
344 {
345 predicate: P,
346 positive_description: D1,
347 negative_description: D2,
348 explain_fn: Option<ExplainFn>,
349 }
350
351 impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
352 where
353 P: Fn(&Value) -> bool,
354 D1: PredicateDescription,
355 D2: PredicateDescription,
356 {
357 pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
358 Self {
359 predicate,
360 positive_description,
361 negative_description,
362 explain_fn: None,
363 }
364 }
365
366 pub fn with_description<D1b, D2b>(
367 self,
368 positive_description: D1b,
369 negative_description: D2b,
370 ) -> JsonPredicateMatcher<P, D1b, D2b>
371 where
372 D1b: PredicateDescription,
373 D2b: PredicateDescription,
374 {
375 JsonPredicateMatcher {
376 predicate: self.predicate,
377 positive_description,
378 negative_description,
379 explain_fn: self.explain_fn,
380 }
381 }
382
383 pub fn with_explain_fn<F>(mut self, f: F) -> Self
384 where
385 F: Fn(&Value) -> Description + 'static,
386 {
387 self.explain_fn = Some(Box::new(f));
388 self
389 }
390 }
391
392 impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
393 where
394 P: Fn(&Value) -> bool,
395 D1: PredicateDescription + Clone,
396 D2: PredicateDescription + Clone,
397 {
398 fn matches(&self, actual: &Value) -> MatcherResult {
399 if (self.predicate)(actual) {
400 Match
401 } else {
402 NoMatch
403 }
404 }
405
406 fn describe(&self, result: MatcherResult) -> Description {
407 let pos = self.positive_description.clone().to_description();
408 let neg = self.negative_description.clone().to_description();
409
410 match result {
411 Match if pos.is_empty() => "matches predicate".into(),
412 NoMatch if neg.is_empty() => "does not match predicate".into(),
413 Match => pos.into(),
414 NoMatch => neg.into(),
415 }
416 }
417
418 fn explain_match(&self, actual: &Value) -> Description {
419 if let Some(ref f) = self.explain_fn {
420 return f(actual);
421 }
422 Description::new().text("which does not match the predicate")
423 }
424 }
425 pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
427 fn allows_missing(&self) -> bool {
429 false
430 }
431 }
432
433 pub trait IntoJsonMatcher<T> {
435 fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
436 }
437
438 impl<J> IntoJsonMatcher<()> for J
439 where
440 J: JsonMatcher + 'static,
441 {
442 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
443 Box::new(self)
444 }
445 }
446
447 #[derive(googletest::matcher::MatcherBase)]
451 struct JsonEqMatcher {
452 expected: Value,
453 }
454
455 impl Matcher<&Value> for JsonEqMatcher {
456 fn matches(&self, actual: &Value) -> MatcherResult {
457 if *actual == self.expected {
458 Match
459 } else {
460 NoMatch
461 }
462 }
463
464 fn describe(&self, result: MatcherResult) -> Description {
465 match result {
466 Match => format!("is equal to {:?}", self.expected).into(),
467 NoMatch => format!("isn't equal to {:?}", self.expected).into(),
468 }
469 }
470
471 fn explain_match(&self, _actual: &Value) -> Description {
472 format!("which isn't equal to {:?}", self.expected).into()
474 }
475 }
476
477 impl JsonMatcher for JsonEqMatcher {}
478
479 impl IntoJsonMatcher<Value> for &Value {
481 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
482 Box::new(JsonEqMatcher {
483 expected: self.clone(),
484 })
485 }
486 }
487
488 impl IntoJsonMatcher<Value> for Value {
489 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
490 Box::new(JsonEqMatcher { expected: self })
491 }
492 }
493
494 pub struct Literal;
496
497 impl IntoJsonMatcher<Literal> for &str {
498 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
499 Box::new(JsonEqMatcher {
500 expected: Value::from(self),
501 })
502 }
503 }
504
505 impl IntoJsonMatcher<Literal> for String {
506 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
507 Box::new(JsonEqMatcher {
508 expected: Value::from(self),
509 })
510 }
511 }
512
513 impl IntoJsonMatcher<Literal> for bool {
514 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
515 Box::new(JsonEqMatcher {
516 expected: Value::from(self),
517 })
518 }
519 }
520
521 impl IntoJsonMatcher<Literal> for i64 {
522 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
523 Box::new(JsonEqMatcher {
524 expected: Value::from(self),
525 })
526 }
527 }
528 impl IntoJsonMatcher<Literal> for i32 {
529 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
530 Box::new(JsonEqMatcher {
531 expected: Value::from(self),
532 })
533 }
534 }
535
536 impl IntoJsonMatcher<Literal> for u64 {
537 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
538 Box::new(JsonEqMatcher {
539 expected: Value::from(self),
540 })
541 }
542 }
543
544 impl IntoJsonMatcher<Literal> for f64 {
545 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
546 Box::new(JsonEqMatcher {
547 expected: Value::from(self),
548 })
549 }
550 }
551
552 impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
553 where
554 P: Fn(&Value) -> bool + 'static,
555 D1: PredicateDescription + Clone + 'static,
556 D2: PredicateDescription + Clone + 'static,
557 {
558 }
559
560 pub fn describe_json_type(v: &Value) -> Description {
561 match v {
562 Value::Null => "which is a JSON null",
563 Value::String(_) => "which is a JSON string",
564 Value::Number(_) => "which is a JSON number",
565 Value::Bool(_) => "which is a JSON boolean",
566 Value::Array(_) => "which is a JSON array",
567 Value::Object(_) => "which is a JSON object",
568 }
569 .into()
570 }
571}