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/// # Examples
14///
15/// Basic usage:
16/// ```
17/// # use googletest::prelude::*;
18/// # use serde_json::json as j;
19/// # use crate::googletest_json_serde::json;
20/// let value = j!(["alex", "bart", "cucumberbatch"]);
21/// assert_that!(
22///     value,
23///     json::elements_are![
24///         j!("alex"),
25///         starts_with("b"),
26///         char_count(eq(13))
27///     ]
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![j!("x"), eq("y")],
41///         json::elements_are![eq("z")]
42///     ]
43/// );
44/// ```
45///
46/// # Notes
47///
48///  - Both JSON-aware and native GoogleTest matchers (such as `starts_with`, `contains_substring`) can be used directly.
49///  - Wrapping with `json::primitive!` is no longer needed.
50///  - Direct `serde_json::Value` inputs (e.g. `json!(...)`) are supported and compared by structural equality.
51#[macro_export]
52#[doc(hidden)]
53macro_rules! __json_elements_are {
54    // Preferred bracketed form: __json_elements_are!([ m1, m2, ... ])
55    ([$($matcher:expr),* $(,)?]) => {{
56        $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonElementsAre::new(vec![
57            $(
58                $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($matcher)
59            ),*
60        ])
61    }};
62    // Convenience: allow unbracketed list and forward to the bracketed arm.
63    ($($matcher:expr),* $(,)?) => {{
64        $crate::__json_elements_are!([$($matcher),*])
65    }};
66}
67
68#[doc(hidden)]
69pub mod internal {
70    use crate::matchers::json_matcher::internal::JsonMatcher;
71    use googletest::description::Description;
72    use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
73    use serde_json::Value;
74
75    #[doc(hidden)]
76    #[derive(MatcherBase)]
77    pub struct JsonElementsAre {
78        elements: Vec<Box<dyn JsonMatcher>>,
79    }
80
81    impl JsonMatcher for JsonElementsAre {}
82
83    impl JsonElementsAre {
84        pub fn new(elements: Vec<Box<dyn JsonMatcher>>) -> Self {
85            Self { elements }
86        }
87    }
88
89    impl Matcher<&Value> for JsonElementsAre {
90        fn matches(&self, actual: &Value) -> MatcherResult {
91            match actual {
92                Value::Array(arr) => {
93                    if arr.len() != self.elements.len() {
94                        return MatcherResult::NoMatch;
95                    }
96                    for (item, matcher) in arr.iter().zip(&self.elements) {
97                        if matcher.matches(item).is_no_match() {
98                            return MatcherResult::NoMatch;
99                        }
100                    }
101                    MatcherResult::Match
102                }
103                _ => MatcherResult::NoMatch,
104            }
105        }
106
107        fn describe(&self, result: MatcherResult) -> Description {
108            let verb = if result.into() { "has" } else { "doesn't have" };
109            let inner = self
110                .elements
111                .iter()
112                .map(|m| m.describe(MatcherResult::Match))
113                .collect::<Description>()
114                .enumerate()
115                .indent();
116
117            format!("{verb} JSON array elements:\n{inner}").into()
118        }
119
120        fn explain_match(&self, actual: &Value) -> Description {
121            match actual {
122                Value::Array(arr) => {
123                    let mut mismatches = Vec::new();
124                    let actual_len = arr.len();
125                    let expected_len = self.elements.len();
126
127                    for (index, (item, matcher)) in arr.iter().zip(&self.elements).enumerate() {
128                        if matcher.matches(item).is_no_match() {
129                            mismatches.push(format!(
130                                "element #{index} is {item:?}, {}",
131                                matcher.explain_match(item)
132                            ));
133                        }
134                    }
135
136                    if mismatches.is_empty() {
137                        if actual_len == expected_len {
138                            "whose elements all match".into()
139                        } else {
140                            format!("whose size is {}", actual_len).into()
141                        }
142                    } else if mismatches.len() == 1 {
143                        let description = mismatches.into_iter().collect::<Description>();
144                        format!("where {description}").into()
145                    } else {
146                        let description = mismatches.into_iter().collect::<Description>();
147                        format!("where:\n{}", description.bullet_list().indent()).into()
148                    }
149                }
150                _ => Description::new().text("where the type is not array".to_string()),
151            }
152        }
153    }
154}