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 for<'a> Matcher<&'a Value>>>,
79 }
80
81 impl JsonMatcher for JsonElementsAre {}
82
83 impl JsonElementsAre {
84 pub fn new(elements: Vec<Box<dyn for<'a> Matcher<&'a Value>>>) -> 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}