googletest_json_serde/matchers/
optional_matcher.rs

1/// Matches a JSON field that may be absent, null, or satisfy an inner matcher.
2///
3/// # Examples
4///
5/// ```rust
6/// # use googletest::prelude::*;
7/// # use googletest_json_serde::json;
8/// # use serde_json::json as j;
9/// let value = j!({ "id": 42 });
10/// assert_that!(
11///     value,
12///     json::pat!({
13///         "id": eq(42),
14///         "nickname": json::optional!("Bob"),
15///         "alt": json::optional!(j!("Robert")),
16///         "tag": json::optional!(starts_with("B"))
17///     })
18/// );
19/// ```
20///
21/// # Supported Inputs
22/// - Literal JSON-compatible values
23/// - Direct `serde_json::Value`
24/// - Native googletest matchers
25#[macro_export]
26#[doc(hidden)]
27macro_rules! __json_optional {
28    ($inner:expr) => {{
29        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonOptionalMatcher::new(
30            $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($inner),
31        )
32    }};
33}
34
35pub mod internal {
36    use crate::matchers::json_matcher::internal::JsonMatcher;
37    use googletest::description::Description;
38    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
39    use serde_json::Value;
40
41    #[derive(MatcherBase)]
42    pub struct JsonOptionalMatcher {
43        inner: Box<dyn JsonMatcher>,
44    }
45
46    impl JsonOptionalMatcher {
47        pub fn new(inner: Box<dyn JsonMatcher>) -> Self {
48            Self { inner }
49        }
50    }
51
52    impl JsonMatcher for JsonOptionalMatcher {
53        fn allows_missing(&self) -> bool {
54            true
55        }
56    }
57
58    impl Matcher<&Value> for JsonOptionalMatcher {
59        fn matches(&self, actual: &Value) -> MatcherResult {
60            if actual.is_null() {
61                MatcherResult::Match
62            } else {
63                self.inner.matches(actual)
64            }
65        }
66
67        fn describe(&self, result: MatcherResult) -> Description {
68            match result {
69                MatcherResult::Match => "is null or matches inner matcher".into(),
70                MatcherResult::NoMatch => "neither null nor matches inner matcher".into(),
71            }
72        }
73
74        fn explain_match(&self, actual: &Value) -> Description {
75            if actual.is_null() {
76                Description::new().text("which is null")
77            } else {
78                self.inner.explain_match(actual)
79            }
80        }
81    }
82}