googletest_json_serde/matchers/
path_matcher.rs1use crate::matcher_support::path::{ParsedPaths, collect_paths, format_path, parse_expected_paths};
2use crate::matchers::__internal_unstable_do_not_depend_on_these;
3use crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPredicateMatcher;
4use googletest::description::Description;
5use serde_json::Value;
6use std::collections::BTreeSet;
7
8#[macro_export]
37#[doc(hidden)]
38macro_rules! __json_has_path_with {
39 ($path:expr, $matcher:expr) => {{
40 $crate::matchers::__internal_unstable_do_not_depend_on_these::JsonPathWithMatcher::new(
41 $path,
42 $crate::matchers::__internal_unstable_do_not_depend_on_these::IntoJsonMatcher::into_json_matcher($matcher),
43 )
44 }};
45}
46
47pub fn has_paths(paths: &[&str]) -> JsonPredicateMatcher<impl Fn(&Value) -> bool, String, String> {
65 let ParsedPaths { parsed, errors } = parse_expected_paths(paths);
66 let expected_set: BTreeSet<_> = parsed.iter().map(|p| p.segments.clone()).collect();
67 let errors_for_explain = errors.clone();
68 let expected_desc = format!(
69 "a JSON object containing paths {:?}",
70 parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
71 );
72 let negative_desc = format!(
73 "which is missing one of {:?}",
74 parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
75 );
76
77 JsonPredicateMatcher::new(
78 {
79 let expected_set = expected_set.clone();
80 let errors = errors.clone();
81 move |v| {
82 if !errors.is_empty() || !v.is_object() {
83 return false;
84 }
85 let actual = collect_paths(v);
86 expected_set.iter().all(|p| actual.contains(p))
87 }
88 },
89 expected_desc,
90 negative_desc,
91 )
92 .with_explain_fn(move |v| {
93 if !errors_for_explain.is_empty() {
94 return Description::new().text(format!(
95 "invalid paths {:?}",
96 errors_for_explain
97 .iter()
98 .map(|e| e.as_str())
99 .collect::<Vec<_>>()
100 ));
101 }
102 if !v.is_object() {
103 return __internal_unstable_do_not_depend_on_these::describe_json_type(v);
104 }
105 let actual = collect_paths(v);
106 let missing: BTreeSet<_> = expected_set.difference(&actual).cloned().collect();
107 if missing.is_empty() {
108 Description::new()
109 } else {
110 Description::new().text(format!(
111 "missing paths {:?}",
112 missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
113 ))
114 }
115 })
116}
117
118pub fn has_only_paths(
134 paths: &[&str],
135) -> JsonPredicateMatcher<impl Fn(&Value) -> bool, String, String> {
136 let ParsedPaths { parsed, errors } = parse_expected_paths(paths);
137 let expected_set: BTreeSet<_> = parsed.iter().map(|p| p.segments.clone()).collect();
138 let errors_for_explain = errors.clone();
139 let expected_desc = format!(
140 "a JSON object with exactly paths {:?}",
141 parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
142 );
143 let negative_desc = format!(
144 "which does not have exactly paths {:?}",
145 parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
146 );
147
148 JsonPredicateMatcher::new(
149 {
150 let expected_set = expected_set.clone();
151 let errors = errors.clone();
152 move |v| {
153 if !errors.is_empty() || !v.is_object() {
154 return false;
155 }
156 let actual = collect_paths(v);
157 actual == expected_set
158 }
159 },
160 expected_desc,
161 negative_desc,
162 )
163 .with_explain_fn(move |v| {
164 if !errors_for_explain.is_empty() {
165 return Description::new().text(format!(
166 "invalid paths {:?}",
167 errors_for_explain
168 .iter()
169 .map(|e| e.as_str())
170 .collect::<Vec<_>>()
171 ));
172 }
173 if !v.is_object() {
174 return __internal_unstable_do_not_depend_on_these::describe_json_type(v);
175 }
176 let actual = collect_paths(v);
177 let missing: BTreeSet<_> = expected_set.difference(&actual).cloned().collect();
178 let extra: BTreeSet<_> = actual.difference(&expected_set).cloned().collect();
179 match (!missing.is_empty(), !extra.is_empty()) {
180 (true, true) => Description::new()
181 .text(format!(
182 "missing paths {:?}",
183 missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
184 ))
185 .text(format!(
186 ", extra paths {:?}",
187 extra.iter().map(|p| format_path(p)).collect::<Vec<_>>()
188 )),
189 (true, false) => Description::new().text(format!(
190 "missing paths {:?}",
191 missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
192 )),
193 (false, true) => Description::new().text(format!(
194 "extra paths {:?}",
195 extra.iter().map(|p| format_path(p)).collect::<Vec<_>>()
196 )),
197 (false, false) => Description::new(),
198 }
199 })
200}
201
202#[doc(hidden)]
203pub mod internal {
204 use crate::matcher_support::path::{PathSegment, format_path, parse_expected_paths};
205 use crate::matchers::__internal_unstable_do_not_depend_on_these::describe_json_type;
206 use crate::matchers::json_matcher::internal::JsonMatcher;
207 use googletest::description::Description;
208 use googletest::matcher::{Matcher, MatcherBase, MatcherResult};
209 use serde_json::Value;
210
211 #[derive(MatcherBase)]
212 pub struct JsonPathWithMatcher {
213 raw: String,
214 segments: Vec<PathSegment>,
215 matcher: Box<dyn JsonMatcher>,
216 parse_error: Option<String>,
217 }
218
219 fn parse_single_path(path: &str) -> (Vec<PathSegment>, Option<String>) {
220 let parsed_paths = parse_expected_paths(&[path]);
221 match (parsed_paths.parsed.first(), parsed_paths.errors.first()) {
222 (_, Some(err)) => (Vec::new(), Some(err.clone())),
223 (Some(p), None) => (p.segments.clone(), None),
224 _ => (Vec::new(), Some("empty path".to_string())),
225 }
226 }
227
228 impl JsonPathWithMatcher {
229 pub fn new(path: &str, matcher: Box<dyn JsonMatcher>) -> Self {
230 let (segments, parse_error) = parse_single_path(path);
231 Self {
232 raw: path.to_string(),
233 segments,
234 matcher,
235 parse_error,
236 }
237 }
238
239 fn find_leaf<'a>(&self, value: &'a Value) -> Option<&'a Value> {
240 let mut current = value;
241 for seg in &self.segments {
242 match (seg, current) {
243 (PathSegment::Field(name), Value::Object(map)) => {
244 current = map.get(name)?;
245 }
246 (PathSegment::Index(idx), Value::Array(arr)) => {
247 current = arr.get(*idx)?;
248 }
249 _ => return None,
250 }
251 }
252 Some(current)
253 }
254 }
255
256 impl Matcher<&Value> for JsonPathWithMatcher {
257 fn matches(&self, value: &Value) -> MatcherResult {
258 if self.parse_error.is_some() {
259 return MatcherResult::NoMatch;
260 }
261 let Some(leaf) = self.find_leaf(value) else {
262 return MatcherResult::NoMatch;
263 };
264 self.matcher.matches(leaf)
265 }
266
267 fn describe(&self, result: MatcherResult) -> Description {
268 let path = &self.raw;
269 match result {
270 MatcherResult::Match => format!("has path `{path}` whose value matches").into(),
271 MatcherResult::NoMatch => {
272 format!("has path `{path}` whose value does not match").into()
273 }
274 }
275 }
276
277 fn explain_match(&self, value: &Value) -> Description {
278 if let Some(err) = &self.parse_error {
279 return Description::new().text(format!("invalid path {err}"));
280 }
281 let Some(leaf) = self.find_leaf(value) else {
282 return match value {
283 Value::Object(_) => Description::new()
284 .text(format!("missing path `{}`", format_path(&self.segments))),
285 _ => describe_json_type(value),
286 };
287 };
288 self.matcher.explain_match(leaf)
289 }
290 }
291}