Skip to main content

jsonette/
query.rs

1/*
2 * Copyright (c) 2026 DevEtte.
3 *
4 * This project is dual-licensed under both the MIT License and the
5 * Apache License, Version 2.0 (the "License"). You may not use this
6 * file except in compliance with one of these licenses.
7 *
8 * You may obtain a copy of the Licenses at:
9 * - MIT: https://opensource.org
10 * - Apache 2.0: http://apache.org
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19//! RFC 9535-conformant JSONPath expression evaluation and path diagnostics.
20//!
21//! This module bridges the span-aware `JsonNode` AST produced by the parser
22//! and the `serde_json::Value` type consumed by `serde_json_path`. Conversion
23//! is done on demand rather than storing a parallel representation, keeping
24//! the memory footprint proportional to the size of query results rather than
25//! the entire document.
26
27use crate::json_node::{JsonNode, KeyValuePair};
28use crate::types::{Diagnostic, Span};
29use serde_json::Value;
30use serde_json_path::JsonPath;
31
32// ─────────────────────────────────────────────────────────────────────────────
33// Internal: JsonNode → serde_json::Value
34// ─────────────────────────────────────────────────────────────────────────────
35
36/// Converts a span-aware `JsonNode` into a `serde_json::Value` for evaluation
37/// by `serde_json_path`. Span metadata is intentionally discarded here because
38/// `serde_json::Value` has no span concept; spans are preserved in the original
39/// tree and not needed during structural traversal.
40///
41/// Numbers are re-serialised from their raw source string to avoid f64
42/// precision loss (e.g. large integers or high-precision decimals).
43///
44/// # Arguments
45///
46/// * `node` - A reference to the `JsonNode` to convert, including any nesting depth.
47///
48/// # Returns
49///
50/// The equivalent `serde_json::Value` representation of the node.
51fn node_to_value(node: &JsonNode) -> Value {
52    match node {
53        JsonNode::Null(_) => Value::Null,
54        JsonNode::Bool(b, _) => Value::Bool(*b),
55        JsonNode::Number(_, raw, _) => raw
56            .parse::<serde_json::Number>()
57            .map(Value::Number)
58            .unwrap_or(Value::Null),
59        JsonNode::String(s, _) => Value::String(s.clone()),
60        JsonNode::Array(items, _) => Value::Array(items.iter().map(node_to_value).collect()),
61        JsonNode::Object(pairs, _) => {
62            let map = pairs
63                .iter()
64                .map(|kv| (kv.key.clone(), node_to_value(&kv.value)))
65                .collect();
66            Value::Object(map)
67        }
68    }
69}
70
71// ─────────────────────────────────────────────────────────────────────────────
72// Internal: serde_json::Value → JsonNode
73// ─────────────────────────────────────────────────────────────────────────────
74
75/// Converts a `serde_json::Value` back into a `JsonNode` after query evaluation.
76///
77/// The resulting nodes carry a zeroed `Span` (`0..0`) because byte offset
78/// information is not recoverable after the round-trip through
79/// `serde_json::Value`. Callers that require source positions should resolve
80/// them against the original parsed tree using the matched value's content.
81///
82/// # Arguments
83///
84/// * `value` - A reference to the `serde_json::Value` to convert back into the engine's node type.
85///
86/// # Returns
87///
88/// A `JsonNode` equivalent to the supplied value, with all spans zeroed.
89fn value_to_node(value: &Value) -> JsonNode {
90    let span = Span::default();
91    match value {
92        Value::Null => JsonNode::Null(span),
93        Value::Bool(b) => JsonNode::Bool(*b, span),
94        Value::Number(n) => JsonNode::Number(n.as_f64().unwrap_or(0.0), n.to_string(), span),
95        Value::String(s) => JsonNode::String(s.clone(), span),
96        Value::Array(items) => JsonNode::Array(items.iter().map(value_to_node).collect(), span),
97        Value::Object(map) => {
98            let pairs = map
99                .iter()
100                .map(|(k, v)| KeyValuePair {
101                    key: k.clone(),
102                    value: value_to_node(v),
103                })
104                .collect();
105            JsonNode::Object(pairs, span)
106        }
107    }
108}
109
110// ─────────────────────────────────────────────────────────────────────────────
111// Public API
112// ─────────────────────────────────────────────────────────────────────────────
113
114/// Evaluates a JSONPath expression against the parsed document AST, returning
115/// all matching nodes as a list of `JsonNode` values.
116///
117/// The expression is first parsed and validated by `serde_json_path`. If it is
118/// syntactically invalid, an `Err` is returned before any document traversal
119/// occurs. Matching is performed on a `serde_json::Value` projection of the
120/// document; each matched value is then converted back to a `JsonNode` for a
121/// consistent return type. Returned nodes carry a zeroed `Span` (`0..0`)
122/// because byte offset information is not recoverable through the
123/// `serde_json::Value` round-trip.
124///
125/// # Arguments
126///
127/// * `node` - A reference to the root `JsonNode` of the document to query against.
128/// * `path` - The RFC 9535 JSONPath expression string to evaluate (e.g. `$.store.book[*].author`).
129///
130/// # Returns
131///
132/// * `Ok(Vec<JsonNode>)` - Zero or more nodes matched by the expression, in document order.
133///   An empty `Vec` indicates the expression is valid but matched nothing.
134/// * `Err(String)` - A human-readable description of the parse error if the expression is syntactically invalid.
135pub fn evaluate_path(node: &JsonNode, path: &str) -> Result<Vec<JsonNode>, String> {
136    let json_path = JsonPath::parse(path).map_err(|e| format!("Invalid JSONPath '{path}': {e}"))?;
137    let value = node_to_value(node);
138    let node_list = json_path.query(&value);
139    let results = node_list.iter().map(|v| value_to_node(v)).collect();
140    Ok(results)
141}
142
143/// Validates a JSONPath expression syntactically without evaluating it against
144/// any document. Intended for live editor feedback where the user is still
145/// composing a query and no document context is available or needed.
146///
147/// The returned diagnostic always spans the entire expression string (`0..len`)
148/// because `serde_json_path` does not expose sub-expression byte offsets.
149///
150/// # Arguments
151///
152/// * `path` - The raw JSONPath expression string slice to validate.
153///
154/// # Returns
155///
156/// * An empty `Vec<Diagnostic>` if the expression is syntactically valid.
157/// * A single-element `Vec<Diagnostic>` whose `span` covers the full input
158///   string and whose `message` describes the parse failure if the expression is invalid.
159pub fn diagnostics_for_path(path: &str) -> Vec<Diagnostic> {
160    match JsonPath::parse(path) {
161        Ok(_) => vec![],
162        Err(e) => vec![Diagnostic {
163            span: Span {
164                start: 0,
165                end: path.len(),
166            },
167            message: format!("Invalid JSONPath: {e}"),
168        }],
169    }
170}
171
172// ─────────────────────────────────────────────────────────────────────────────
173// Tests
174// ─────────────────────────────────────────────────────────────────────────────
175
176#[cfg(test)]
177mod jsonpath_tests {
178    use super::*;
179    use crate::parser::parse;
180
181    /// Parses a trusted test document string, panicking on failure.
182    fn parse_doc(src: &str) -> JsonNode {
183        parse(src).expect("test document must be valid JSON")
184    }
185
186    // ── evaluate_path ─────────────────────────────────────────────────────────
187
188    /// **Test Case**: Root Selector Returns Entire Document
189    ///
190    /// ### Description
191    /// Verifies that the bare root selector `$` returns the complete document
192    /// as a single-element result list.
193    ///
194    /// ### Test Procedure
195    /// 1. Parse a simple JSON object `{"a": 1}`.
196    /// 2. Evaluate the path `$`.
197    ///
198    /// ### Expected Result
199    /// Returns exactly one `JsonNode::Object` matching the root document.
200    #[test]
201    fn test_jsonpath_root_selector() {
202        let node = parse_doc(r#"{"a": 1}"#);
203        let results = evaluate_path(&node, "$").unwrap();
204        assert_eq!(results.len(), 1);
205        assert!(matches!(results[0], JsonNode::Object(_, _)));
206    }
207
208    /// **Test Case**: Dot-Key Selector Returns Named Property
209    ///
210    /// ### Description
211    /// Verifies that a single dot-key selector (`$.key`) correctly extracts the
212    /// value of a named top-level property.
213    ///
214    /// ### Test Procedure
215    /// 1. Parse `{"name": "Alice", "age": 30}`.
216    /// 2. Evaluate `$.name`.
217    ///
218    /// ### Expected Result
219    /// Returns a single `JsonNode::String("Alice")` with a zeroed span.
220    #[test]
221    fn test_jsonpath_dot_key() {
222        let node = parse_doc(r#"{"name": "Alice", "age": 30}"#);
223        let results = evaluate_path(&node, "$.name").unwrap();
224        assert_eq!(results.len(), 1);
225        assert_eq!(
226            results[0],
227            JsonNode::String("Alice".into(), Span::default())
228        );
229    }
230
231    /// **Test Case**: Array Index Selector Returns Indexed Element
232    ///
233    /// ### Description
234    /// Verifies that bracket index notation (`[0]`) selects the correct element
235    /// from a nested array.
236    ///
237    /// ### Test Procedure
238    /// 1. Parse `{"arr": [10, 20, 30]}`.
239    /// 2. Evaluate `$.arr[0]`.
240    ///
241    /// ### Expected Result
242    /// Returns a single `JsonNode::Number(10.0, "10")`.
243    #[test]
244    fn test_jsonpath_array_index() {
245        let node = parse_doc(r#"{"arr": [10, 20, 30]}"#);
246        let results = evaluate_path(&node, "$.arr[0]").unwrap();
247        assert_eq!(results.len(), 1);
248        assert_eq!(
249            results[0],
250            JsonNode::Number(10.0, "10".into(), Span::default())
251        );
252    }
253
254    /// **Test Case**: Wildcard Selector Collects All Matching Array Items
255    ///
256    /// ### Description
257    /// Verifies the key use case from the issue specification: wildcard traversal
258    /// of an object array collecting named properties from each element.
259    ///
260    /// ### Test Procedure
261    /// 1. Parse `{"users": [{"name": "Alice"}, {"name": "Bob"}]}`.
262    /// 2. Evaluate `$.users[*].name`.
263    ///
264    /// ### Expected Result
265    /// Returns two string nodes `"Alice"` and `"Bob"` in document order.
266    #[test]
267    fn test_jsonpath_wildcard_array() {
268        let node = parse_doc(r#"{"users": [{"name": "Alice"}, {"name": "Bob"}]}"#);
269        let results = evaluate_path(&node, "$.users[*].name").unwrap();
270        assert_eq!(results.len(), 2);
271        assert_eq!(
272            results[0],
273            JsonNode::String("Alice".into(), Span::default())
274        );
275        assert_eq!(results[1], JsonNode::String("Bob".into(), Span::default()));
276    }
277
278    /// **Test Case**: Recursive Descent Selector Finds Keys at Any Depth
279    ///
280    /// ### Description
281    /// Verifies that the `..` (descendant) operator locates matching keys
282    /// at all nesting levels within the document tree.
283    ///
284    /// ### Test Procedure
285    /// 1. Parse a document with `"name"` at root level and deeply nested.
286    /// 2. Evaluate `$..name`.
287    ///
288    /// ### Expected Result
289    /// Returns both occurrences of the `name` key, regardless of depth.
290    #[test]
291    fn test_jsonpath_recursive_descent() {
292        let node = parse_doc(r#"{"a": {"b": {"name": "deep"}}, "name": "root"}"#);
293        let results = evaluate_path(&node, "$..name").unwrap();
294        assert_eq!(results.len(), 2);
295    }
296
297    /// **Test Case**: Filter Expression Selects Elements by Predicate
298    ///
299    /// ### Description
300    /// Verifies that a filter expression (`?@.field > value`) correctly filters
301    /// array elements based on a comparison against a numeric field.
302    ///
303    /// ### Test Procedure
304    /// 1. Parse `{"items": [{"val": 1}, {"val": 5}, {"val": 3}]}`.
305    /// 2. Evaluate `$.items[?@.val > 2]`.
306    ///
307    /// ### Expected Result
308    /// Returns two objects (those with `val` equal to 5 and 3).
309    #[test]
310    fn test_jsonpath_filter_expression() {
311        let node = parse_doc(r#"{"items": [{"val": 1}, {"val": 5}, {"val": 3}]}"#);
312        let results = evaluate_path(&node, "$.items[?@.val > 2]").unwrap();
313        assert_eq!(results.len(), 2);
314    }
315
316    /// **Test Case**: Non-Matching Path Returns Empty Result List
317    ///
318    /// ### Description
319    /// Verifies that querying for a key that does not exist in the document
320    /// returns an empty result list without error, consistent with RFC 9535.
321    ///
322    /// ### Test Procedure
323    /// 1. Parse `{"a": 1}`.
324    /// 2. Evaluate `$.nonexistent`.
325    ///
326    /// ### Expected Result
327    /// Returns `Ok` with an empty `Vec`.
328    #[test]
329    fn test_jsonpath_no_match_returns_empty() {
330        let node = parse_doc(r#"{"a": 1}"#);
331        let results = evaluate_path(&node, "$.nonexistent").unwrap();
332        assert!(results.is_empty());
333    }
334
335    /// **Test Case**: Invalid JSONPath Expression Returns Descriptive Error
336    ///
337    /// ### Description
338    /// Verifies that a syntactically invalid JSONPath expression produces an
339    /// `Err` result rather than panicking or silently returning no results.
340    ///
341    /// ### Test Procedure
342    /// 1. Parse a minimal valid JSON document.
343    /// 2. Evaluate a string that is not a valid JSONPath expression.
344    ///
345    /// ### Expected Result
346    /// Returns `Err` with a message containing `"Invalid JSONPath"`.
347    #[test]
348    fn test_jsonpath_invalid_path_returns_err() {
349        let node = parse_doc(r#"{"a": 1}"#);
350        let result = evaluate_path(&node, "not a valid path");
351        assert!(result.is_err());
352        assert!(result.unwrap_err().contains("Invalid JSONPath"));
353    }
354
355    /// **Test Case**: Null Value Is Preserved Through Conversion Round-Trip
356    ///
357    /// ### Description
358    /// Verifies that a JSON `null` value selected by a query is correctly
359    /// converted back to `JsonNode::Null` after the `serde_json::Value`
360    /// round-trip.
361    ///
362    /// ### Test Procedure
363    /// 1. Parse `{"x": null}`.
364    /// 2. Evaluate `$.x`.
365    ///
366    /// ### Expected Result
367    /// Returns a single `JsonNode::Null`.
368    #[test]
369    fn test_jsonpath_null_value() {
370        let node = parse_doc(r#"{"x": null}"#);
371        let results = evaluate_path(&node, "$.x").unwrap();
372        assert_eq!(results.len(), 1);
373        assert!(matches!(results[0], JsonNode::Null(_)));
374    }
375
376    /// **Test Case**: Boolean Value Is Preserved Through Conversion Round-Trip
377    ///
378    /// ### Description
379    /// Verifies that a JSON boolean value selected by a query is correctly
380    /// converted back to `JsonNode::Bool` after the `serde_json::Value`
381    /// round-trip.
382    ///
383    /// ### Test Procedure
384    /// 1. Parse `{"flag": true}`.
385    /// 2. Evaluate `$.flag`.
386    ///
387    /// ### Expected Result
388    /// Returns a single `JsonNode::Bool(true)`.
389    #[test]
390    fn test_jsonpath_bool_value() {
391        let node = parse_doc(r#"{"flag": true}"#);
392        let results = evaluate_path(&node, "$.flag").unwrap();
393        assert_eq!(results.len(), 1);
394        assert_eq!(results[0], JsonNode::Bool(true, Span::default()));
395    }
396
397    // ── diagnostics_for_path ──────────────────────────────────────────────────
398
399    /// **Test Case**: Valid Complex Path Produces No Diagnostics
400    ///
401    /// ### Description
402    /// Verifies that a syntactically correct and complete JSONPath expression
403    /// produces an empty diagnostics list.
404    ///
405    /// ### Test Procedure
406    /// 1. Call `diagnostics_for_path` with `$.store.book[*].author`.
407    ///
408    /// ### Expected Result
409    /// Returns an empty `Vec<Diagnostic>`.
410    #[test]
411    fn test_diagnostics_valid_path_is_empty() {
412        let diags = diagnostics_for_path("$.store.book[*].author");
413        assert!(diags.is_empty());
414    }
415
416    /// **Test Case**: Root-Only Selector Produces No Diagnostics
417    ///
418    /// ### Description
419    /// Verifies that the minimal valid JSONPath expression (`$`) does not
420    /// produce any diagnostic errors.
421    ///
422    /// ### Test Procedure
423    /// 1. Call `diagnostics_for_path` with `$`.
424    ///
425    /// ### Expected Result
426    /// Returns an empty `Vec<Diagnostic>`.
427    #[test]
428    fn test_diagnostics_root_only_is_valid() {
429        let diags = diagnostics_for_path("$");
430        assert!(diags.is_empty());
431    }
432
433    /// **Test Case**: Invalid Expression Returns a Single Spanning Diagnostic
434    ///
435    /// ### Description
436    /// Verifies that a syntactically invalid expression produces exactly one
437    /// diagnostic whose span covers the full input string and whose message
438    /// contains a useful prefix.
439    ///
440    /// ### Test Procedure
441    /// 1. Call `diagnostics_for_path` with an arbitrary non-JSONPath string.
442    ///
443    /// ### Expected Result
444    /// Returns one `Diagnostic` with `span.start == 0`, `span.end == len(input)`,
445    /// and a message containing `"Invalid JSONPath"`.
446    #[test]
447    fn test_diagnostics_invalid_path_returns_diagnostic() {
448        let input = "INVALID";
449        let diags = diagnostics_for_path(input);
450        assert_eq!(diags.len(), 1);
451        assert!(diags[0].message.contains("Invalid JSONPath"));
452        assert_eq!(diags[0].span.start, 0);
453        assert_eq!(diags[0].span.end, input.len());
454    }
455
456    /// **Test Case**: Empty String Expression Returns a Diagnostic
457    ///
458    /// ### Description
459    /// Verifies that an empty string — which is not a valid JSONPath expression
460    /// since every path must begin with `$` — produces at least one diagnostic.
461    ///
462    /// ### Test Procedure
463    /// 1. Call `diagnostics_for_path` with an empty string `""`.
464    ///
465    /// ### Expected Result
466    /// Returns a non-empty `Vec<Diagnostic>`.
467    #[test]
468    fn test_diagnostics_empty_string_returns_diagnostic() {
469        let diags = diagnostics_for_path("");
470        assert!(!diags.is_empty());
471    }
472
473    // ── Conversion round-trip ─────────────────────────────────────────────────
474
475    /// **Test Case**: Object Node Survives JsonNode → Value → JsonNode Round-Trip
476    ///
477    /// ### Description
478    /// Verifies that converting a parsed `JsonNode::Object` to `serde_json::Value`
479    /// and back to `JsonNode` preserves the structural shape and key names,
480    /// even though source spans are zeroed out in the returned copy.
481    ///
482    /// ### Test Procedure
483    /// 1. Parse `{"key": "value", "num": 42}`.
484    /// 2. Convert to `serde_json::Value` via `node_to_value`.
485    /// 3. Convert back via `value_to_node`.
486    ///
487    /// ### Expected Result
488    /// The round-tripped node is a `JsonNode::Object` with two key-value pairs;
489    /// the first key is `"key"`.
490    #[test]
491    fn test_node_to_value_and_back_object() {
492        let node = parse_doc(r#"{"key": "value", "num": 42}"#);
493        let value = node_to_value(&node);
494        let roundtrip = value_to_node(&value);
495        assert!(matches!(roundtrip, JsonNode::Object(_, _)));
496        if let JsonNode::Object(pairs, _) = &roundtrip {
497            assert_eq!(pairs.len(), 2);
498            assert_eq!(pairs[0].key, "key");
499        }
500    }
501}