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}