googletest_json_serde/matchers/
elements_are_matcher.rs

1/// Matches a JSON array with elements that satisfy the given matchers, in order.
2///
3/// Each element of the JSON array is matched against a corresponding
4/// [`Matcher`][googletest::matcher::Matcher]. The array must have the same length
5/// as the list of matchers, and all matchers must succeed.
6///
7/// This macro supports two forms:
8/// - Bracketed: `elements_are!([matcher1, matcher2, ...])`
9/// - Unbracketed: `elements_are!(matcher1, matcher2, ...)`
10///
11/// Callers should prefer the public-facing [`json::elements_are!`](crate::json::elements_are!) macro.
12///
13/// # Notes
14///
15/// Both JSON-aware and native GoogleTest matchers (such as `starts_with`, `contains_substring`) can be used directly.
16/// Wrapping with `json::primitive!` is no longer needed.
17///
18/// # Example
19///
20/// ```
21/// # use googletest::prelude::*;
22/// # use serde_json::json as j;
23/// # use crate::googletest_json_serde::json;
24/// let value = j!(["alex", "bart", "cucumberbatch"]);
25/// assert_that!(
26///     value,
27///     json::elements_are![starts_with("a"), eq("bart"), char_count(eq(13))]
28/// );
29/// ```
30///
31/// Nested example:
32/// ```
33/// # use googletest::prelude::*;
34/// # use serde_json::json as j;
35/// # use crate::googletest_json_serde::json;
36/// let value = j!([["x", "y"], ["z"]]);
37/// assert_that!(
38///     value,
39///     json::elements_are![
40///         json::elements_are![eq("x"), eq("y")],
41///         json::elements_are![eq("z")]
42///     ]
43/// );
44/// ```
45///
46/// # See also
47///
48/// [`googletest::matcher::Matcher`], [`crate::json::elements_are!`]
49#[macro_export]
50#[doc(hidden)]
51macro_rules! __json_elements_are {
52    // Preferred bracketed form: __json_elements_are!([ m1, m2, ... ])
53    ([$($matcher:expr),* $(,)?]) => {{
54        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonElementsAre::new(vec![
55            $(
56                $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($matcher)
57            ),*
58        ])
59    }};
60    // Convenience: allow unbracketed list and forward to the bracketed arm.
61    ($($matcher:expr),* $(,)?) => {{
62        $crate::__json_elements_are!([$($matcher),*])
63    }};
64}
65
66#[doc(hidden)]
67pub mod internal {
68    use crate::matchers::json_matcher::internal::JsonMatcher;
69    use googletest::description::Description;
70    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
71    use serde_json::Value;
72
73    #[doc(hidden)]
74    #[derive(MatcherBase)]
75    pub struct JsonElementsAre {
76        elements: Vec<Box<dyn for<'a> Matcher<&'a Value>>>,
77    }
78
79    impl JsonMatcher for JsonElementsAre {}
80
81    impl JsonElementsAre {
82        pub fn new(elements: Vec<Box<dyn for<'a> Matcher<&'a Value>>>) -> Self {
83            Self { elements }
84        }
85    }
86
87    impl Matcher<&Value> for JsonElementsAre {
88        fn matches(&self, actual: &Value) -> MatcherResult {
89            match actual {
90                Value::Array(arr) => {
91                    if arr.len() != self.elements.len() {
92                        return MatcherResult::NoMatch;
93                    }
94                    for (item, matcher) in arr.iter().zip(&self.elements) {
95                        if matcher.matches(item).is_no_match() {
96                            return MatcherResult::NoMatch;
97                        }
98                    }
99                    MatcherResult::Match
100                }
101                _ => MatcherResult::NoMatch,
102            }
103        }
104
105        fn describe(&self, result: MatcherResult) -> Description {
106            let verb = if result.into() { "has" } else { "doesn't have" };
107            let inner = self
108                .elements
109                .iter()
110                .map(|m| m.describe(MatcherResult::Match))
111                .collect::<Description>()
112                .enumerate()
113                .indent();
114
115            format!("{verb} JSON array elements:\n{inner}").into()
116        }
117
118        fn explain_match(&self, actual: &Value) -> Description {
119            match actual {
120                Value::Array(arr) => {
121                    let mut mismatches = Vec::new();
122                    let actual_len = arr.len();
123                    let expected_len = self.elements.len();
124
125                    for (index, (item, matcher)) in arr.iter().zip(&self.elements).enumerate() {
126                        if matcher.matches(item).is_no_match() {
127                            mismatches.push(format!(
128                                "element #{index} is {item:?}, {}",
129                                matcher.explain_match(item)
130                            ));
131                        }
132                    }
133
134                    if mismatches.is_empty() {
135                        if actual_len == expected_len {
136                            "whose elements all match".into()
137                        } else {
138                            format!("whose size is {}", actual_len).into()
139                        }
140                    } else if mismatches.len() == 1 {
141                        let description = mismatches.into_iter().collect::<Description>();
142                        format!("where {description}").into()
143                    } else {
144                        let description = mismatches.into_iter().collect::<Description>();
145                        format!("where:\n{}", description.bullet_list().indent()).into()
146                    }
147                }
148                _ => Description::new().text("where the type is not array".to_string()),
149            }
150        }
151    }
152}