googletest_json_serde/matchers/
path_matcher.rs

1use 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/// Matches a JSON leaf at the given path against the provided matcher.
9///
10/// The path uses the same dot-and-escape rules as [`has_paths`]. The matcher can be a literal,
11/// a `serde_json::Value`, or any native googletest matcher.
12///
13/// # Examples
14///
15/// ```rust
16/// # use googletest::prelude::*;
17/// # use googletest_json_serde::json;
18/// # use serde_json::json as j;
19/// let value = j!({"user": {"id": 7, "name": "Ada"}});
20/// assert_that!(
21///     value,
22///     json::has_path_with!("user.name", "Ada")
23///         .and(json::has_path_with!("user.id", j!(7)))
24///         .and(json::has_path_with!("user.name", starts_with("A")))
25/// );
26/// ```
27///
28/// # Supported Inputs
29/// - Literal JSON-compatible values
30/// - Direct `serde_json::Value`
31/// - Native googletest matchers
32///
33/// # Errors
34/// Fails when the path is invalid, missing, the value is not an object, or the leaf does not
35/// satisfy the matcher.
36#[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
47/// Matches a JSON object that contains all specified paths (order-agnostic, extras allowed).
48///
49/// Paths use dot notation; escape dots inside field names with `\`.
50///
51/// # Examples
52///
53/// ```rust
54/// # use googletest::prelude::*;
55/// # use googletest_json_serde::json;
56/// # use serde_json::json as j;
57/// let value = j!({"user": {"id": 7, "name": "Ada"}});
58/// assert_that!(value, json::has_paths(&["user.id", "user.name"]));
59/// ```
60///
61/// # Errors
62///
63/// Fails when any path is invalid, when the value is not a JSON object, or when required paths are missing.
64pub 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
118/// Matches a JSON object whose paths are exactly the provided set (no extras or missing).
119///
120/// # Examples
121///
122/// ```rust
123/// # use googletest::prelude::*;
124/// # use googletest_json_serde::json;
125/// # use serde_json::json as j;
126/// let value = j!({"ids": [1, 2], "ok": true});
127/// assert_that!(value, json::has_only_paths(&["ids", "ids.0", "ids.1", "ok"]));
128/// ```
129///
130/// # Errors
131///
132/// Fails when any path is invalid, when the value is not a JSON object, or when the set of paths differs.
133pub 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}