Skip to main content

wallfacer_core/property/
jsonpath.rs

1//! Thin façade over [`serde_json_path`] (RFC 9535) preserving the legacy
2//! `resolve` / `resolve_one` API used by the property runner.
3//!
4//! Phase B1: this module no longer parses paths itself. We delegate to the
5//! `serde_json_path` crate and just adapt the result types and errors.
6
7use serde_json::Value;
8use thiserror::Error;
9
10/// Errors returned by path resolution.
11#[derive(Debug, Error)]
12pub enum JsonPathError {
13    /// The path string is not valid RFC 9535 JSONPath syntax.
14    #[error("invalid JSONPath `{path}`: {source}")]
15    Parse {
16        path: String,
17        #[source]
18        source: serde_json_path::ParseError,
19    },
20    /// The path parsed successfully but resolved to nothing in the document.
21    #[error("path `{0}` did not resolve to any value")]
22    Missing(String),
23}
24
25pub type Result<T> = std::result::Result<T, JsonPathError>;
26
27/// Resolves all matches for a JSONPath expression. Returns an owned vector;
28/// callers retain no borrow on `root`.
29pub fn resolve(root: &Value, path: &str) -> Result<Vec<Value>> {
30    let parsed = serde_json_path::JsonPath::parse(path).map_err(|source| JsonPathError::Parse {
31        path: path.to_string(),
32        source,
33    })?;
34    Ok(parsed
35        .query(root)
36        .all()
37        .into_iter()
38        .cloned()
39        .collect::<Vec<_>>())
40}
41
42/// Resolves a JSONPath expression and returns the first match. Returns
43/// [`JsonPathError::Missing`] if the path resolved to zero nodes.
44pub fn resolve_one(root: &Value, path: &str) -> Result<Value> {
45    let nodes = resolve(root, path)?;
46    nodes
47        .into_iter()
48        .next()
49        .ok_or_else(|| JsonPathError::Missing(path.to_string()))
50}
51
52#[cfg(test)]
53#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
54mod tests {
55    use super::*;
56    use serde_json::json;
57
58    #[test]
59    fn resolves_nested_field() {
60        let root = json!({"a": {"b": 42}});
61        assert_eq!(resolve_one(&root, "$.a.b").unwrap(), json!(42));
62    }
63
64    #[test]
65    fn resolves_array_index() {
66        let root = json!({"items": [10, 20, 30]});
67        assert_eq!(resolve_one(&root, "$.items[1]").unwrap(), json!(20));
68    }
69
70    #[test]
71    fn resolves_wildcard_in_middle() {
72        // RFC 9535 supports wildcards anywhere; the previous home-grown parser
73        // forbade non-final wildcards. This is a regression test that the new
74        // backend lifts that limit.
75        let root = json!({"items": [{"v": 1}, {"v": 2}, {"v": 3}]});
76        let values = resolve(&root, "$.items[*].v").unwrap();
77        assert_eq!(values, vec![json!(1), json!(2), json!(3)]);
78    }
79
80    #[test]
81    fn missing_returns_missing_error() {
82        let root = json!({"a": 1});
83        let err = resolve_one(&root, "$.nope").unwrap_err();
84        assert!(matches!(err, JsonPathError::Missing(_)));
85    }
86
87    #[test]
88    fn invalid_syntax_returns_parse_error() {
89        let root = json!({});
90        let err = resolve(&root, "not-a-path").unwrap_err();
91        assert!(matches!(err, JsonPathError::Parse { .. }));
92    }
93
94    #[test]
95    fn filter_expression_works() {
96        let root = json!({"items": [{"v": 1}, {"v": 2}, {"v": 3}]});
97        let values = resolve(&root, "$.items[?@.v > 1].v").unwrap();
98        assert_eq!(values, vec![json!(2), json!(3)]);
99    }
100}