1use 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_empty_string() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
124{
125 JsonPredicateMatcher::new(
126 |v| v.as_str().is_some_and(|s| s.is_empty()),
127 "an empty JSON string",
128 "which is not an empty JSON string",
129 )
130 .with_explain_fn(|v| {
131 if v.is_string() {
132 Description::new().text("which is a non-empty JSON string")
133 } else {
134 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
135 }
136 })
137}
138
139pub fn is_non_empty_string()
151-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
152 JsonPredicateMatcher::new(
153 |v| v.as_str().is_some_and(|s| !s.is_empty()),
154 "a non-empty JSON string",
155 "which is not a non-empty JSON string",
156 )
157 .with_explain_fn(|v| {
158 if v.is_string() {
159 Description::new().text("which is an empty JSON string")
160 } else {
161 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
162 }
163 })
164}
165
166pub fn is_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
178 JsonPredicateMatcher::new(
179 |v| v.is_number(),
180 "a JSON number",
181 "which is not a JSON number",
182 )
183 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
184}
185
186pub fn is_integer() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
198 JsonPredicateMatcher::new(
199 |v| matches!(v, Value::Number(n) if n.is_i64() || n.is_u64()),
200 "an integer JSON number",
201 "which is not an integer JSON number",
202 )
203 .with_explain_fn(|v| {
204 if matches!(v, Value::Number(_)) {
205 Description::new().text("which is a non-integer JSON number")
206 } else {
207 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
208 }
209 })
210}
211
212pub fn is_whole_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
224{
225 JsonPredicateMatcher::new(
226 |v| match v {
227 Value::Number(n) => {
228 if n.is_i64() || n.is_u64() {
229 true
230 } else {
231 n.as_f64()
232 .is_some_and(|f| f.is_finite() && f.fract() == 0.0)
233 }
234 }
235 _ => false,
236 },
237 "a JSON number with no fractional part",
238 "which is not a JSON number with no fractional part",
239 )
240 .with_explain_fn(|v| {
241 if matches!(v, Value::Number(_)) {
242 Description::new().text("which is a JSON number with a fractional part")
243 } else {
244 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
245 }
246 })
247}
248
249pub fn is_fractional_number()
262-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
263 JsonPredicateMatcher::new(
264 |v| match v {
265 Value::Number(n) => {
266 if n.is_i64() || n.is_u64() {
267 return false;
268 }
269 n.as_f64()
270 .is_some_and(|f| f.is_finite() && f.fract() != 0.0)
271 }
272 _ => false,
273 },
274 "a JSON number with a fractional part",
275 "which is not a JSON number with a fractional part",
276 )
277 .with_explain_fn(|v| {
278 if matches!(v, Value::Number(_)) {
279 Description::new().text("which is a JSON number without a fractional part")
280 } else {
281 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
282 }
283 })
284}
285
286pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
298 JsonPredicateMatcher::new(
299 |v| v.is_boolean(),
300 "a JSON boolean",
301 "which is not a JSON boolean",
302 )
303 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
304}
305
306pub fn is_true() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
318 JsonPredicateMatcher::new(
319 |v| matches!(v, Value::Bool(true)),
320 "JSON true",
321 "which is not JSON true",
322 )
323 .with_explain_fn(|v| match v {
324 Value::Bool(false) => Description::new().text("which is JSON false"),
325 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
326 })
327}
328
329pub fn is_false() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
341 JsonPredicateMatcher::new(
342 |v| matches!(v, Value::Bool(false)),
343 "JSON false",
344 "which is not JSON false",
345 )
346 .with_explain_fn(|v| match v {
347 Value::Bool(true) => Description::new().text("which is JSON true"),
348 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
349 })
350}
351
352pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
364 JsonPredicateMatcher::new(
365 |v| v.is_array(),
366 "a JSON array",
367 "which is not a JSON array",
368 )
369 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
370}
371
372pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
384{
385 JsonPredicateMatcher::new(
386 |v| v.as_array().is_some_and(|a| a.is_empty()),
387 "an empty JSON array",
388 "which is not an empty JSON array",
389 )
390 .with_explain_fn(|v| {
391 if v.is_array() {
392 Description::new().text("which is a non-empty JSON array")
393 } else {
394 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
395 }
396 })
397}
398
399pub fn is_non_empty_array()
411-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
412 JsonPredicateMatcher::new(
413 |v| v.as_array().is_some_and(|a| !a.is_empty()),
414 "a non-empty JSON array",
415 "which is not a non-empty JSON array",
416 )
417 .with_explain_fn(|v| {
418 if v.is_array() {
419 Description::new().text("which is an empty JSON array")
420 } else {
421 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
422 }
423 })
424}
425
426pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
438 JsonPredicateMatcher::new(
439 |v| v.is_object(),
440 "a JSON object",
441 "which is not a JSON object",
442 )
443 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
444}
445
446pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
458{
459 JsonPredicateMatcher::new(
460 |v| v.as_object().is_some_and(|o| o.is_empty()),
461 "an empty JSON object",
462 "which is not an empty JSON object",
463 )
464 .with_explain_fn(|v| {
465 if v.is_object() {
466 Description::new().text("which is a non-empty JSON object")
467 } else {
468 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
469 }
470 })
471}
472
473pub fn is_non_empty_object()
485-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
486 JsonPredicateMatcher::new(
487 |v| v.as_object().is_some_and(|o| !o.is_empty()),
488 "a non-empty JSON object",
489 "which is not a non-empty JSON object",
490 )
491 .with_explain_fn(|v| {
492 if v.is_object() {
493 Description::new().text("which is an empty JSON object")
494 } else {
495 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
496 }
497 })
498}
499
500#[doc(hidden)]
503pub mod internal {
504 use googletest::description::Description;
505 use googletest::matcher::MatcherResult::{Match, NoMatch};
506 use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
507 use serde_json::Value;
508
509 pub trait PredicateDescription {
511 fn to_description(self) -> String;
512 }
513
514 impl PredicateDescription for &'static str {
515 fn to_description(self) -> String {
516 self.to_string()
517 }
518 }
519
520 impl PredicateDescription for String {
521 fn to_description(self) -> String {
522 self
523 }
524 }
525
526 impl<F> PredicateDescription for F
527 where
528 F: Fn() -> String,
529 {
530 fn to_description(self) -> String {
531 self()
532 }
533 }
534 #[derive(Clone, Copy, Debug)]
536 pub struct NoDescription;
537 impl PredicateDescription for NoDescription {
538 fn to_description(self) -> String {
539 String::new()
540 }
541 }
542
543 type ExplainFn = Box<dyn Fn(&Value) -> Description>;
545
546 #[derive(MatcherBase)]
547 pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
548 where
549 P: Fn(&Value) -> bool,
550 D1: PredicateDescription,
551 D2: PredicateDescription,
552 {
553 predicate: P,
554 positive_description: D1,
555 negative_description: D2,
556 explain_fn: Option<ExplainFn>,
557 }
558
559 impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
560 where
561 P: Fn(&Value) -> bool,
562 D1: PredicateDescription,
563 D2: PredicateDescription,
564 {
565 pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
566 Self {
567 predicate,
568 positive_description,
569 negative_description,
570 explain_fn: None,
571 }
572 }
573
574 pub fn with_description<D1b, D2b>(
575 self,
576 positive_description: D1b,
577 negative_description: D2b,
578 ) -> JsonPredicateMatcher<P, D1b, D2b>
579 where
580 D1b: PredicateDescription,
581 D2b: PredicateDescription,
582 {
583 JsonPredicateMatcher {
584 predicate: self.predicate,
585 positive_description,
586 negative_description,
587 explain_fn: self.explain_fn,
588 }
589 }
590
591 pub fn with_explain_fn<F>(mut self, f: F) -> Self
592 where
593 F: Fn(&Value) -> Description + 'static,
594 {
595 self.explain_fn = Some(Box::new(f));
596 self
597 }
598 }
599
600 impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
601 where
602 P: Fn(&Value) -> bool,
603 D1: PredicateDescription + Clone,
604 D2: PredicateDescription + Clone,
605 {
606 fn matches(&self, actual: &Value) -> MatcherResult {
607 if (self.predicate)(actual) {
608 Match
609 } else {
610 NoMatch
611 }
612 }
613
614 fn describe(&self, result: MatcherResult) -> Description {
615 let pos = self.positive_description.clone().to_description();
616 let neg = self.negative_description.clone().to_description();
617
618 match result {
619 Match if pos.is_empty() => "matches predicate".into(),
620 NoMatch if neg.is_empty() => "does not match predicate".into(),
621 Match => pos.into(),
622 NoMatch => neg.into(),
623 }
624 }
625
626 fn explain_match(&self, actual: &Value) -> Description {
627 if let Some(ref f) = self.explain_fn {
628 return f(actual);
629 }
630 Description::new().text("which does not match the predicate")
631 }
632 }
633 pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
635 fn allows_missing(&self) -> bool {
637 false
638 }
639 }
640
641 pub trait IntoJsonMatcher<T> {
643 fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
644 }
645
646 impl<J> IntoJsonMatcher<()> for J
647 where
648 J: JsonMatcher + 'static,
649 {
650 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
651 Box::new(self)
652 }
653 }
654
655 #[derive(googletest::matcher::MatcherBase)]
659 struct JsonEqMatcher {
660 expected: Value,
661 }
662
663 impl Matcher<&Value> for JsonEqMatcher {
664 fn matches(&self, actual: &Value) -> MatcherResult {
665 if *actual == self.expected {
666 Match
667 } else {
668 NoMatch
669 }
670 }
671
672 fn describe(&self, result: MatcherResult) -> Description {
673 match result {
674 Match => format!("is equal to {:?}", self.expected).into(),
675 NoMatch => format!("isn't equal to {:?}", self.expected).into(),
676 }
677 }
678
679 fn explain_match(&self, _actual: &Value) -> Description {
680 format!("which isn't equal to {:?}", self.expected).into()
682 }
683 }
684
685 impl JsonMatcher for JsonEqMatcher {}
686
687 impl IntoJsonMatcher<Value> for &Value {
689 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
690 Box::new(JsonEqMatcher {
691 expected: self.clone(),
692 })
693 }
694 }
695
696 impl IntoJsonMatcher<Value> for Value {
697 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
698 Box::new(JsonEqMatcher { expected: self })
699 }
700 }
701
702 pub struct Literal;
704
705 impl IntoJsonMatcher<Literal> for &str {
706 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
707 Box::new(JsonEqMatcher {
708 expected: Value::from(self),
709 })
710 }
711 }
712
713 impl IntoJsonMatcher<Literal> for String {
714 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
715 Box::new(JsonEqMatcher {
716 expected: Value::from(self),
717 })
718 }
719 }
720
721 impl IntoJsonMatcher<Literal> for bool {
722 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
723 Box::new(JsonEqMatcher {
724 expected: Value::from(self),
725 })
726 }
727 }
728
729 impl IntoJsonMatcher<Literal> for i64 {
730 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
731 Box::new(JsonEqMatcher {
732 expected: Value::from(self),
733 })
734 }
735 }
736 impl IntoJsonMatcher<Literal> for i32 {
737 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
738 Box::new(JsonEqMatcher {
739 expected: Value::from(self),
740 })
741 }
742 }
743
744 impl IntoJsonMatcher<Literal> for u64 {
745 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
746 Box::new(JsonEqMatcher {
747 expected: Value::from(self),
748 })
749 }
750 }
751
752 impl IntoJsonMatcher<Literal> for f64 {
753 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
754 Box::new(JsonEqMatcher {
755 expected: Value::from(self),
756 })
757 }
758 }
759
760 impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
761 where
762 P: Fn(&Value) -> bool + 'static,
763 D1: PredicateDescription + Clone + 'static,
764 D2: PredicateDescription + Clone + 'static,
765 {
766 }
767
768 pub fn describe_json_type(v: &Value) -> Description {
769 match v {
770 Value::Null => "which is a JSON null",
771 Value::String(_) => "which is a JSON string",
772 Value::Number(_) => "which is a JSON number",
773 Value::Bool(_) => "which is a JSON boolean",
774 Value::Array(_) => "which is a JSON array",
775 Value::Object(_) => "which is a JSON object",
776 }
777 .into()
778 }
779}