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_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_integer() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
144 JsonPredicateMatcher::new(
145 |v| matches!(v, Value::Number(n) if n.is_i64() || n.is_u64()),
146 "an integer JSON number",
147 "which is not an integer JSON number",
148 )
149 .with_explain_fn(|v| {
150 if matches!(v, Value::Number(_)) {
151 Description::new().text("which is a non-integer JSON number")
152 } else {
153 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
154 }
155 })
156}
157
158pub fn is_whole_number() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
170{
171 JsonPredicateMatcher::new(
172 |v| match v {
173 Value::Number(n) => {
174 if n.is_i64() || n.is_u64() {
175 true
176 } else {
177 n.as_f64()
178 .is_some_and(|f| f.is_finite() && f.fract() == 0.0)
179 }
180 }
181 _ => false,
182 },
183 "a JSON number with no fractional part",
184 "which is not a JSON number with no fractional part",
185 )
186 .with_explain_fn(|v| {
187 if matches!(v, Value::Number(_)) {
188 Description::new().text("which is a JSON number with a fractional part")
189 } else {
190 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
191 }
192 })
193}
194
195pub fn is_fractional_number()
208-> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
209 JsonPredicateMatcher::new(
210 |v| match v {
211 Value::Number(n) => {
212 if n.is_i64() || n.is_u64() {
213 return false;
214 }
215 n.as_f64()
216 .is_some_and(|f| f.is_finite() && f.fract() != 0.0)
217 }
218 _ => false,
219 },
220 "a JSON number with a fractional part",
221 "which is not a JSON number with a fractional part",
222 )
223 .with_explain_fn(|v| {
224 if matches!(v, Value::Number(_)) {
225 Description::new().text("which is a JSON number without a fractional part")
226 } else {
227 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
228 }
229 })
230}
231
232pub fn is_boolean() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
244 JsonPredicateMatcher::new(
245 |v| v.is_boolean(),
246 "a JSON boolean",
247 "which is not a JSON boolean",
248 )
249 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
250}
251
252pub fn is_true() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
264 JsonPredicateMatcher::new(
265 |v| matches!(v, Value::Bool(true)),
266 "JSON true",
267 "which is not JSON true",
268 )
269 .with_explain_fn(|v| match v {
270 Value::Bool(false) => Description::new().text("which is JSON false"),
271 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
272 })
273}
274
275pub fn is_false() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
287 JsonPredicateMatcher::new(
288 |v| matches!(v, Value::Bool(false)),
289 "JSON false",
290 "which is not JSON false",
291 )
292 .with_explain_fn(|v| match v {
293 Value::Bool(true) => Description::new().text("which is JSON true"),
294 _ => __internal_unstable_do_not_depend_on_these::describe_json_type(v),
295 })
296}
297
298pub fn is_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
310 JsonPredicateMatcher::new(
311 |v| v.is_array(),
312 "a JSON array",
313 "which is not a JSON array",
314 )
315 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
316}
317
318pub fn is_empty_array() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
330{
331 JsonPredicateMatcher::new(
332 |v| v.as_array().is_some_and(|a| a.is_empty()),
333 "an empty JSON array",
334 "which is not an empty JSON array",
335 )
336 .with_explain_fn(|v| {
337 if v.is_array() {
338 Description::new().text("which is a non-empty JSON array")
339 } else {
340 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
341 }
342 })
343}
344
345pub fn is_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str> {
357 JsonPredicateMatcher::new(
358 |v| v.is_object(),
359 "a JSON object",
360 "which is not a JSON object",
361 )
362 .with_explain_fn(__internal_unstable_do_not_depend_on_these::describe_json_type)
363}
364
365pub fn is_empty_object() -> JsonPredicateMatcher<impl Fn(&Value) -> bool, &'static str, &'static str>
377{
378 JsonPredicateMatcher::new(
379 |v| v.as_object().is_some_and(|o| o.is_empty()),
380 "an empty JSON object",
381 "which is not an empty JSON object",
382 )
383 .with_explain_fn(|v| {
384 if v.is_object() {
385 Description::new().text("which is a non-empty JSON object")
386 } else {
387 __internal_unstable_do_not_depend_on_these::describe_json_type(v)
388 }
389 })
390}
391
392#[doc(hidden)]
395pub mod internal {
396 use googletest::description::Description;
397 use googletest::matcher::MatcherResult::{Match, NoMatch};
398 use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
399 use serde_json::Value;
400
401 pub trait PredicateDescription {
403 fn to_description(self) -> String;
404 }
405
406 impl PredicateDescription for &'static str {
407 fn to_description(self) -> String {
408 self.to_string()
409 }
410 }
411
412 impl PredicateDescription for String {
413 fn to_description(self) -> String {
414 self
415 }
416 }
417
418 impl<F> PredicateDescription for F
419 where
420 F: Fn() -> String,
421 {
422 fn to_description(self) -> String {
423 self()
424 }
425 }
426 #[derive(Clone, Copy, Debug)]
428 pub struct NoDescription;
429 impl PredicateDescription for NoDescription {
430 fn to_description(self) -> String {
431 String::new()
432 }
433 }
434
435 type ExplainFn = Box<dyn Fn(&Value) -> Description>;
437
438 #[derive(MatcherBase)]
439 pub struct JsonPredicateMatcher<P, D1 = NoDescription, D2 = NoDescription>
440 where
441 P: Fn(&Value) -> bool,
442 D1: PredicateDescription,
443 D2: PredicateDescription,
444 {
445 predicate: P,
446 positive_description: D1,
447 negative_description: D2,
448 explain_fn: Option<ExplainFn>,
449 }
450
451 impl<P, D1, D2> JsonPredicateMatcher<P, D1, D2>
452 where
453 P: Fn(&Value) -> bool,
454 D1: PredicateDescription,
455 D2: PredicateDescription,
456 {
457 pub fn new(predicate: P, positive_description: D1, negative_description: D2) -> Self {
458 Self {
459 predicate,
460 positive_description,
461 negative_description,
462 explain_fn: None,
463 }
464 }
465
466 pub fn with_description<D1b, D2b>(
467 self,
468 positive_description: D1b,
469 negative_description: D2b,
470 ) -> JsonPredicateMatcher<P, D1b, D2b>
471 where
472 D1b: PredicateDescription,
473 D2b: PredicateDescription,
474 {
475 JsonPredicateMatcher {
476 predicate: self.predicate,
477 positive_description,
478 negative_description,
479 explain_fn: self.explain_fn,
480 }
481 }
482
483 pub fn with_explain_fn<F>(mut self, f: F) -> Self
484 where
485 F: Fn(&Value) -> Description + 'static,
486 {
487 self.explain_fn = Some(Box::new(f));
488 self
489 }
490 }
491
492 impl<P, D1, D2> Matcher<&Value> for JsonPredicateMatcher<P, D1, D2>
493 where
494 P: Fn(&Value) -> bool,
495 D1: PredicateDescription + Clone,
496 D2: PredicateDescription + Clone,
497 {
498 fn matches(&self, actual: &Value) -> MatcherResult {
499 if (self.predicate)(actual) {
500 Match
501 } else {
502 NoMatch
503 }
504 }
505
506 fn describe(&self, result: MatcherResult) -> Description {
507 let pos = self.positive_description.clone().to_description();
508 let neg = self.negative_description.clone().to_description();
509
510 match result {
511 Match if pos.is_empty() => "matches predicate".into(),
512 NoMatch if neg.is_empty() => "does not match predicate".into(),
513 Match => pos.into(),
514 NoMatch => neg.into(),
515 }
516 }
517
518 fn explain_match(&self, actual: &Value) -> Description {
519 if let Some(ref f) = self.explain_fn {
520 return f(actual);
521 }
522 Description::new().text("which does not match the predicate")
523 }
524 }
525 pub trait JsonMatcher: for<'a> Matcher<&'a Value> {
527 fn allows_missing(&self) -> bool {
529 false
530 }
531 }
532
533 pub trait IntoJsonMatcher<T> {
535 fn into_json_matcher(self) -> Box<dyn JsonMatcher>;
536 }
537
538 impl<J> IntoJsonMatcher<()> for J
539 where
540 J: JsonMatcher + 'static,
541 {
542 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
543 Box::new(self)
544 }
545 }
546
547 #[derive(googletest::matcher::MatcherBase)]
551 struct JsonEqMatcher {
552 expected: Value,
553 }
554
555 impl Matcher<&Value> for JsonEqMatcher {
556 fn matches(&self, actual: &Value) -> MatcherResult {
557 if *actual == self.expected {
558 Match
559 } else {
560 NoMatch
561 }
562 }
563
564 fn describe(&self, result: MatcherResult) -> Description {
565 match result {
566 Match => format!("is equal to {:?}", self.expected).into(),
567 NoMatch => format!("isn't equal to {:?}", self.expected).into(),
568 }
569 }
570
571 fn explain_match(&self, _actual: &Value) -> Description {
572 format!("which isn't equal to {:?}", self.expected).into()
574 }
575 }
576
577 impl JsonMatcher for JsonEqMatcher {}
578
579 impl IntoJsonMatcher<Value> for &Value {
581 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
582 Box::new(JsonEqMatcher {
583 expected: self.clone(),
584 })
585 }
586 }
587
588 impl IntoJsonMatcher<Value> for Value {
589 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
590 Box::new(JsonEqMatcher { expected: self })
591 }
592 }
593
594 pub struct Literal;
596
597 impl IntoJsonMatcher<Literal> for &str {
598 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
599 Box::new(JsonEqMatcher {
600 expected: Value::from(self),
601 })
602 }
603 }
604
605 impl IntoJsonMatcher<Literal> for String {
606 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
607 Box::new(JsonEqMatcher {
608 expected: Value::from(self),
609 })
610 }
611 }
612
613 impl IntoJsonMatcher<Literal> for bool {
614 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
615 Box::new(JsonEqMatcher {
616 expected: Value::from(self),
617 })
618 }
619 }
620
621 impl IntoJsonMatcher<Literal> for i64 {
622 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
623 Box::new(JsonEqMatcher {
624 expected: Value::from(self),
625 })
626 }
627 }
628 impl IntoJsonMatcher<Literal> for i32 {
629 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
630 Box::new(JsonEqMatcher {
631 expected: Value::from(self),
632 })
633 }
634 }
635
636 impl IntoJsonMatcher<Literal> for u64 {
637 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
638 Box::new(JsonEqMatcher {
639 expected: Value::from(self),
640 })
641 }
642 }
643
644 impl IntoJsonMatcher<Literal> for f64 {
645 fn into_json_matcher(self) -> Box<dyn JsonMatcher> {
646 Box::new(JsonEqMatcher {
647 expected: Value::from(self),
648 })
649 }
650 }
651
652 impl<P, D1, D2> JsonMatcher for JsonPredicateMatcher<P, D1, D2>
653 where
654 P: Fn(&Value) -> bool + 'static,
655 D1: PredicateDescription + Clone + 'static,
656 D2: PredicateDescription + Clone + 'static,
657 {
658 }
659
660 pub fn describe_json_type(v: &Value) -> Description {
661 match v {
662 Value::Null => "which is a JSON null",
663 Value::String(_) => "which is a JSON string",
664 Value::Number(_) => "which is a JSON number",
665 Value::Bool(_) => "which is a JSON boolean",
666 Value::Array(_) => "which is a JSON array",
667 Value::Object(_) => "which is a JSON object",
668 }
669 .into()
670 }
671}