googletest_json_serde/matchers/
path_matcher.rs

1use crate::json::__internal_unstable_do_not_depend_on_these;
2use crate::matcher_support::path::{ParsedPaths, collect_paths, format_path, parse_expected_paths};
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 object that contains all the specified paths (order-agnostic, extras allowed).
9pub fn has_paths(paths: &[&str]) -> JsonPredicateMatcher<impl Fn(&Value) -> bool, String, String> {
10    let ParsedPaths { parsed, errors } = parse_expected_paths(paths);
11    let expected_set: BTreeSet<_> = parsed.iter().map(|p| p.segments.clone()).collect();
12    let errors_for_explain = errors.clone();
13    let expected_desc = format!(
14        "a JSON object containing paths {:?}",
15        parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
16    );
17    let negative_desc = format!(
18        "which is missing one of {:?}",
19        parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
20    );
21
22    JsonPredicateMatcher::new(
23        {
24            let expected_set = expected_set.clone();
25            let errors = errors.clone();
26            move |v| {
27                if !errors.is_empty() || !v.is_object() {
28                    return false;
29                }
30                let actual = collect_paths(v);
31                expected_set.iter().all(|p| actual.contains(p))
32            }
33        },
34        expected_desc,
35        negative_desc,
36    )
37    .with_explain_fn(move |v| {
38        if !errors_for_explain.is_empty() {
39            return Description::new().text(format!(
40                "invalid paths {:?}",
41                errors_for_explain
42                    .iter()
43                    .map(|e| e.as_str())
44                    .collect::<Vec<_>>()
45            ));
46        }
47        if !v.is_object() {
48            return __internal_unstable_do_not_depend_on_these::describe_json_type(v);
49        }
50        let actual = collect_paths(v);
51        let missing: BTreeSet<_> = expected_set.difference(&actual).cloned().collect();
52        if missing.is_empty() {
53            Description::new()
54        } else {
55            Description::new().text(format!(
56                "missing paths {:?}",
57                missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
58            ))
59        }
60    })
61}
62
63/// Matches a JSON object whose paths are exactly the provided set (no extras or missing).
64pub fn has_only_paths(
65    paths: &[&str],
66) -> JsonPredicateMatcher<impl Fn(&Value) -> bool, String, String> {
67    let ParsedPaths { parsed, errors } = parse_expected_paths(paths);
68    let expected_set: BTreeSet<_> = parsed.iter().map(|p| p.segments.clone()).collect();
69    let errors_for_explain = errors.clone();
70    let expected_desc = format!(
71        "a JSON object with exactly paths {:?}",
72        parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
73    );
74    let negative_desc = format!(
75        "which does not have exactly paths {:?}",
76        parsed.iter().map(|p| &p.raw).collect::<Vec<_>>()
77    );
78
79    JsonPredicateMatcher::new(
80        {
81            let expected_set = expected_set.clone();
82            let errors = errors.clone();
83            move |v| {
84                if !errors.is_empty() || !v.is_object() {
85                    return false;
86                }
87                let actual = collect_paths(v);
88                actual == expected_set
89            }
90        },
91        expected_desc,
92        negative_desc,
93    )
94    .with_explain_fn(move |v| {
95        if !errors_for_explain.is_empty() {
96            return Description::new().text(format!(
97                "invalid paths {:?}",
98                errors_for_explain
99                    .iter()
100                    .map(|e| e.as_str())
101                    .collect::<Vec<_>>()
102            ));
103        }
104        if !v.is_object() {
105            return __internal_unstable_do_not_depend_on_these::describe_json_type(v);
106        }
107        let actual = collect_paths(v);
108        let missing: BTreeSet<_> = expected_set.difference(&actual).cloned().collect();
109        let extra: BTreeSet<_> = actual.difference(&expected_set).cloned().collect();
110        match (!missing.is_empty(), !extra.is_empty()) {
111            (true, true) => Description::new()
112                .text(format!(
113                    "missing paths {:?}",
114                    missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
115                ))
116                .text(format!(
117                    ", extra paths {:?}",
118                    extra.iter().map(|p| format_path(p)).collect::<Vec<_>>()
119                )),
120            (true, false) => Description::new().text(format!(
121                "missing paths {:?}",
122                missing.iter().map(|p| format_path(p)).collect::<Vec<_>>()
123            )),
124            (false, true) => Description::new().text(format!(
125                "extra paths {:?}",
126                extra.iter().map(|p| format_path(p)).collect::<Vec<_>>()
127            )),
128            (false, false) => Description::new(),
129        }
130    })
131}