1use crate::JpxEngine;
7use crate::error::{EngineError, Result};
8use jpx_core::ast::{Ast, Comparator};
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ExplainResult {
28 pub expression: String,
30 pub steps: Vec<ExplainStep>,
32 pub functions_used: Vec<String>,
34 pub complexity: String,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ExplainStep {
41 pub node_type: String,
43 pub description: String,
45 #[serde(skip_serializing_if = "Vec::is_empty")]
47 pub children: Vec<ExplainStep>,
48}
49
50impl JpxEngine {
51 pub fn explain(&self, expression: &str) -> Result<ExplainResult> {
76 let ast = jpx_core::parse(expression)
77 .map_err(|e| EngineError::InvalidExpression(e.to_string()))?;
78
79 let mut functions = Vec::new();
80 let steps = vec![walk_ast(&ast, &mut functions)];
81
82 functions.sort();
83 functions.dedup();
84
85 let complexity = classify_complexity(&ast);
86
87 Ok(ExplainResult {
88 expression: expression.to_string(),
89 steps,
90 functions_used: functions,
91 complexity,
92 })
93 }
94}
95
96fn walk_ast(node: &Ast, functions: &mut Vec<String>) -> ExplainStep {
98 match node {
99 Ast::Identity { .. } => ExplainStep {
100 node_type: "identity".into(),
101 description: "Reference the current node (@)".into(),
102 children: vec![],
103 },
104 Ast::Field { name, .. } => ExplainStep {
105 node_type: "field".into(),
106 description: format!("Select the '{}' field", name),
107 children: vec![],
108 },
109 Ast::Index { idx, .. } => ExplainStep {
110 node_type: "index".into(),
111 description: if *idx < 0 {
112 format!("Select element at index {} (from end)", idx)
113 } else {
114 format!("Select element at index {}", idx)
115 },
116 children: vec![],
117 },
118 Ast::Slice {
119 start, stop, step, ..
120 } => {
121 let start_s = start.map_or(String::new(), |s| s.to_string());
122 let stop_s = stop.map_or(String::new(), |s| s.to_string());
123 let desc = if *step == 1 {
124 format!("Slice array [{}:{}]", start_s, stop_s)
125 } else {
126 format!("Slice array [{}:{}:{}]", start_s, stop_s, step)
127 };
128 ExplainStep {
129 node_type: "slice".into(),
130 description: desc,
131 children: vec![],
132 }
133 }
134 Ast::Subexpr { lhs, rhs, .. } => {
135 let left = walk_ast(lhs, functions);
136 let right = walk_ast(rhs, functions);
137
138 ExplainStep {
141 node_type: "subexpression".into(),
142 description: "Chain two expressions (left.right)".into(),
143 children: vec![left, right],
144 }
145 }
146 Ast::Projection { lhs, rhs, .. } => {
147 let source = walk_ast(lhs, functions);
148 let project = walk_ast(rhs, functions);
149 ExplainStep {
150 node_type: "projection".into(),
151 description: "Project: evaluate right side for each element of left side".into(),
152 children: vec![source, project],
153 }
154 }
155 Ast::Function { name, args, .. } => {
156 functions.push(name.clone());
157 let arg_steps: Vec<ExplainStep> = args.iter().map(|a| walk_ast(a, functions)).collect();
158 let desc = if args.is_empty() {
159 format!("Call function {}()", name)
160 } else {
161 format!("Call function {}() with {} argument(s)", name, args.len())
162 };
163 ExplainStep {
164 node_type: "function".into(),
165 description: desc,
166 children: arg_steps,
167 }
168 }
169 Ast::Literal { value, .. } => {
170 let json = serde_json::to_string(value).unwrap_or_else(|_| "?".into());
171 ExplainStep {
172 node_type: "literal".into(),
173 description: format!("Literal value: {}", json),
174 children: vec![],
175 }
176 }
177 Ast::Comparison {
178 comparator,
179 lhs,
180 rhs,
181 ..
182 } => {
183 let op = match comparator {
184 Comparator::Equal => "==",
185 Comparator::NotEqual => "!=",
186 Comparator::LessThan => "<",
187 Comparator::LessThanEqual => "<=",
188 Comparator::GreaterThan => ">",
189 Comparator::GreaterThanEqual => ">=",
190 };
191 let left = walk_ast(lhs, functions);
192 let right = walk_ast(rhs, functions);
193 ExplainStep {
194 node_type: "comparison".into(),
195 description: format!("Compare using {}", op),
196 children: vec![left, right],
197 }
198 }
199 Ast::And { lhs, rhs, .. } => {
200 let left = walk_ast(lhs, functions);
201 let right = walk_ast(rhs, functions);
202 ExplainStep {
203 node_type: "and".into(),
204 description: "Logical AND: both sides must be truthy".into(),
205 children: vec![left, right],
206 }
207 }
208 Ast::Or { lhs, rhs, .. } => {
209 let left = walk_ast(lhs, functions);
210 let right = walk_ast(rhs, functions);
211 ExplainStep {
212 node_type: "or".into(),
213 description: "Logical OR: return left if truthy, else right".into(),
214 children: vec![left, right],
215 }
216 }
217 Ast::Not { node, .. } => {
218 let inner = walk_ast(node, functions);
219 ExplainStep {
220 node_type: "not".into(),
221 description: "Logical NOT: negate the result".into(),
222 children: vec![inner],
223 }
224 }
225 Ast::Condition {
226 predicate, then, ..
227 } => {
228 let pred = walk_ast(predicate, functions);
229 let body = walk_ast(then, functions);
230 ExplainStep {
231 node_type: "filter".into(),
232 description: "Filter elements matching a condition".into(),
233 children: vec![pred, body],
234 }
235 }
236 Ast::Flatten { node, .. } => {
237 let inner = walk_ast(node, functions);
238 ExplainStep {
239 node_type: "flatten".into(),
240 description: "Flatten nested arrays by one level".into(),
241 children: vec![inner],
242 }
243 }
244 Ast::ObjectValues { node, .. } => {
245 let inner = walk_ast(node, functions);
246 ExplainStep {
247 node_type: "object_values".into(),
248 description: "Extract all values from an object".into(),
249 children: vec![inner],
250 }
251 }
252 Ast::MultiList { elements, .. } => {
253 let children: Vec<ExplainStep> =
254 elements.iter().map(|e| walk_ast(e, functions)).collect();
255 ExplainStep {
256 node_type: "multi_select_list".into(),
257 description: format!("Create a list of {} evaluated expressions", elements.len()),
258 children,
259 }
260 }
261 Ast::MultiHash { elements, .. } => {
262 let children: Vec<ExplainStep> = elements
263 .iter()
264 .map(|kvp| {
265 let mut step = walk_ast(&kvp.value, functions);
266 step.description = format!("'{}': {}", kvp.key, step.description);
267 step
268 })
269 .collect();
270 ExplainStep {
271 node_type: "multi_select_hash".into(),
272 description: format!("Create an object with {} key(s)", elements.len()),
273 children,
274 }
275 }
276 Ast::Expref { ast, .. } => {
277 let inner = walk_ast(ast, functions);
278 ExplainStep {
279 node_type: "expression_reference".into(),
280 description: "Pass expression as argument (used by sort_by, map, etc.)".into(),
281 children: vec![inner],
282 }
283 }
284 Ast::VariableRef { name, .. } => ExplainStep {
285 node_type: "variable_ref".into(),
286 description: format!("Reference variable ${}", name),
287 children: vec![],
288 },
289 Ast::Let { bindings, expr, .. } => {
290 let mut children: Vec<ExplainStep> = bindings
291 .iter()
292 .map(|(name, ast)| {
293 let mut step = walk_ast(ast, functions);
294 step.description = format!("${} = {}", name, step.description);
295 step
296 })
297 .collect();
298 children.push(walk_ast(expr, functions));
299 ExplainStep {
300 node_type: "let".into(),
301 description: format!("Bind {} variable(s) and evaluate body", bindings.len()),
302 children,
303 }
304 }
305 }
306}
307
308fn classify_complexity(ast: &Ast) -> String {
310 let depth = ast_depth(ast);
311 let func_count = count_functions(ast);
312 let has_filter = uses_filter(ast);
313
314 if depth <= 2 && func_count == 0 && !has_filter {
315 "simple".into()
316 } else if depth <= 5 && func_count <= 2 {
317 "moderate".into()
318 } else {
319 "complex".into()
320 }
321}
322
323fn ast_depth(node: &Ast) -> usize {
324 match node {
325 Ast::Identity { .. }
326 | Ast::Field { .. }
327 | Ast::Index { .. }
328 | Ast::Slice { .. }
329 | Ast::Literal { .. } => 1,
330 Ast::Subexpr { lhs, rhs, .. }
331 | Ast::Projection { lhs, rhs, .. }
332 | Ast::And { lhs, rhs, .. }
333 | Ast::Or { lhs, rhs, .. }
334 | Ast::Comparison { lhs, rhs, .. } => 1 + ast_depth(lhs).max(ast_depth(rhs)),
335 Ast::Condition {
336 predicate, then, ..
337 } => 1 + ast_depth(predicate).max(ast_depth(then)),
338 Ast::Not { node, .. } | Ast::Flatten { node, .. } | Ast::ObjectValues { node, .. } => {
339 1 + ast_depth(node)
340 }
341 Ast::Function { args, .. } => 1 + args.iter().map(ast_depth).max().unwrap_or(0),
342 Ast::MultiList { elements, .. } => 1 + elements.iter().map(ast_depth).max().unwrap_or(0),
343 Ast::MultiHash { elements, .. } => {
344 1 + elements
345 .iter()
346 .map(|kvp| ast_depth(&kvp.value))
347 .max()
348 .unwrap_or(0)
349 }
350 Ast::Expref { ast, .. } => 1 + ast_depth(ast),
351 Ast::VariableRef { .. } => 1,
352 Ast::Let { bindings, expr, .. } => {
353 let binding_depth = bindings
354 .iter()
355 .map(|(_, ast)| ast_depth(ast))
356 .max()
357 .unwrap_or(0);
358 1 + binding_depth.max(ast_depth(expr))
359 }
360 }
361}
362
363fn count_functions(node: &Ast) -> usize {
364 match node {
365 Ast::Identity { .. }
366 | Ast::Field { .. }
367 | Ast::Index { .. }
368 | Ast::Slice { .. }
369 | Ast::Literal { .. } => 0,
370 Ast::Subexpr { lhs, rhs, .. }
371 | Ast::Projection { lhs, rhs, .. }
372 | Ast::And { lhs, rhs, .. }
373 | Ast::Or { lhs, rhs, .. }
374 | Ast::Comparison { lhs, rhs, .. } => count_functions(lhs) + count_functions(rhs),
375 Ast::Condition {
376 predicate, then, ..
377 } => count_functions(predicate) + count_functions(then),
378 Ast::Not { node, .. } | Ast::Flatten { node, .. } | Ast::ObjectValues { node, .. } => {
379 count_functions(node)
380 }
381 Ast::Function { args, .. } => 1 + args.iter().map(count_functions).sum::<usize>(),
382 Ast::MultiList { elements, .. } => elements.iter().map(count_functions).sum(),
383 Ast::MultiHash { elements, .. } => {
384 elements.iter().map(|kvp| count_functions(&kvp.value)).sum()
385 }
386 Ast::Expref { ast, .. } => count_functions(ast),
387 Ast::VariableRef { .. } => 0,
388 Ast::Let { bindings, expr, .. } => {
389 bindings
390 .iter()
391 .map(|(_, ast)| count_functions(ast))
392 .sum::<usize>()
393 + count_functions(expr)
394 }
395 }
396}
397
398fn uses_filter(node: &Ast) -> bool {
399 match node {
400 Ast::Condition { .. } => true,
401 Ast::Identity { .. }
402 | Ast::Field { .. }
403 | Ast::Index { .. }
404 | Ast::Slice { .. }
405 | Ast::Literal { .. } => false,
406 Ast::Subexpr { lhs, rhs, .. }
407 | Ast::Projection { lhs, rhs, .. }
408 | Ast::And { lhs, rhs, .. }
409 | Ast::Or { lhs, rhs, .. }
410 | Ast::Comparison { lhs, rhs, .. } => uses_filter(lhs) || uses_filter(rhs),
411 Ast::Not { node, .. } | Ast::Flatten { node, .. } | Ast::ObjectValues { node, .. } => {
412 uses_filter(node)
413 }
414 Ast::Function { args, .. } => args.iter().any(uses_filter),
415 Ast::MultiList { elements, .. } => elements.iter().any(uses_filter),
416 Ast::MultiHash { elements, .. } => elements.iter().any(|kvp| uses_filter(&kvp.value)),
417 Ast::Expref { ast, .. } => uses_filter(ast),
418 Ast::VariableRef { .. } => false,
419 Ast::Let { bindings, expr, .. } => {
420 bindings.iter().any(|(_, ast)| uses_filter(ast)) || uses_filter(expr)
421 }
422 }
423}
424
425pub fn has_let_nodes(node: &Ast) -> bool {
430 match node {
431 Ast::VariableRef { .. } | Ast::Let { .. } => true,
432 Ast::Identity { .. }
433 | Ast::Field { .. }
434 | Ast::Index { .. }
435 | Ast::Slice { .. }
436 | Ast::Literal { .. } => false,
437 Ast::Subexpr { lhs, rhs, .. }
438 | Ast::Projection { lhs, rhs, .. }
439 | Ast::And { lhs, rhs, .. }
440 | Ast::Or { lhs, rhs, .. }
441 | Ast::Comparison { lhs, rhs, .. } => has_let_nodes(lhs) || has_let_nodes(rhs),
442 Ast::Condition {
443 predicate, then, ..
444 } => has_let_nodes(predicate) || has_let_nodes(then),
445 Ast::Not { node, .. } | Ast::Flatten { node, .. } | Ast::ObjectValues { node, .. } => {
446 has_let_nodes(node)
447 }
448 Ast::Function { args, .. } => args.iter().any(has_let_nodes),
449 Ast::MultiList { elements, .. } => elements.iter().any(has_let_nodes),
450 Ast::MultiHash { elements, .. } => elements.iter().any(|kvp| has_let_nodes(&kvp.value)),
451 Ast::Expref { ast, .. } => has_let_nodes(ast),
452 }
453}
454
455pub fn collect_function_names(node: &Ast) -> Vec<String> {
461 let mut names = Vec::new();
462 collect_functions_recursive(node, &mut names);
463 names.sort();
464 names.dedup();
465 names
466}
467
468fn collect_functions_recursive(node: &Ast, names: &mut Vec<String>) {
469 match node {
470 Ast::Identity { .. }
471 | Ast::Field { .. }
472 | Ast::Index { .. }
473 | Ast::Slice { .. }
474 | Ast::Literal { .. }
475 | Ast::VariableRef { .. } => {}
476 Ast::Subexpr { lhs, rhs, .. }
477 | Ast::Projection { lhs, rhs, .. }
478 | Ast::And { lhs, rhs, .. }
479 | Ast::Or { lhs, rhs, .. }
480 | Ast::Comparison { lhs, rhs, .. } => {
481 collect_functions_recursive(lhs, names);
482 collect_functions_recursive(rhs, names);
483 }
484 Ast::Condition {
485 predicate, then, ..
486 } => {
487 collect_functions_recursive(predicate, names);
488 collect_functions_recursive(then, names);
489 }
490 Ast::Not { node, .. } | Ast::Flatten { node, .. } | Ast::ObjectValues { node, .. } => {
491 collect_functions_recursive(node, names);
492 }
493 Ast::Function { name, args, .. } => {
494 names.push(name.clone());
495 for arg in args {
496 collect_functions_recursive(arg, names);
497 }
498 }
499 Ast::MultiList { elements, .. } => {
500 for e in elements {
501 collect_functions_recursive(e, names);
502 }
503 }
504 Ast::MultiHash { elements, .. } => {
505 for kvp in elements {
506 collect_functions_recursive(&kvp.value, names);
507 }
508 }
509 Ast::Expref { ast, .. } => collect_functions_recursive(ast, names),
510 Ast::Let { bindings, expr, .. } => {
511 for (_, ast) in bindings {
512 collect_functions_recursive(ast, names);
513 }
514 collect_functions_recursive(expr, names);
515 }
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 fn engine() -> JpxEngine {
524 JpxEngine::new()
525 }
526
527 #[test]
528 fn test_simple_field() {
529 let result = engine().explain("name").unwrap();
530 assert_eq!(result.steps[0].node_type, "field");
531 assert!(result.steps[0].description.contains("name"));
532 assert!(result.functions_used.is_empty());
533 assert_eq!(result.complexity, "simple");
534 }
535
536 #[test]
537 fn test_filter_expression() {
538 let result = engine().explain("users[?age > `30`]").unwrap();
539 assert!(!result.steps.is_empty());
541 assert_eq!(result.complexity, "moderate");
542 }
543
544 #[test]
545 fn test_projection() {
546 let result = engine().explain("users[*].name").unwrap();
547 assert_eq!(result.steps[0].node_type, "projection");
548 assert!(result.functions_used.is_empty());
549 }
550
551 #[test]
552 fn test_pipe_with_function() {
553 let result = engine().explain("users[*].name | sort(@)").unwrap();
554 assert!(result.functions_used.contains(&"sort".to_string()));
555 assert_eq!(result.complexity, "moderate");
556 }
557
558 #[test]
559 fn test_multi_select() {
560 let result = engine().explain("{name: name, age: age}").unwrap();
561 assert_eq!(result.steps[0].node_type, "multi_select_hash");
562 assert_eq!(result.steps[0].children.len(), 2);
563 }
564
565 #[test]
566 fn test_complex_expression() {
567 let result = engine()
568 .explain("users[?active].addresses[*].city | sort(@) | join(', ', @)")
569 .unwrap();
570 assert!(result.functions_used.contains(&"sort".to_string()));
571 assert!(result.functions_used.contains(&"join".to_string()));
572 assert_eq!(result.complexity, "complex");
573 }
574
575 #[test]
576 fn test_invalid_expression() {
577 let err = engine().explain("users[*.name");
578 assert!(err.is_err());
579 }
580
581 #[test]
582 fn test_identity() {
583 let result = engine().explain("@").unwrap();
584 assert_eq!(result.steps[0].node_type, "identity");
585 assert_eq!(result.complexity, "simple");
586 }
587
588 #[test]
589 fn test_index() {
590 let result = engine().explain("[0]").unwrap();
591 assert_eq!(result.steps[0].node_type, "index");
592 }
593
594 #[test]
595 fn test_flatten() {
596 let result = engine().explain("items[]").unwrap();
597 assert!(!result.steps.is_empty());
599 }
600
601 fn contains_node_type(step: &ExplainStep, target: &str) -> bool {
603 if step.node_type == target {
604 return true;
605 }
606 step.children.iter().any(|c| contains_node_type(c, target))
607 }
608
609 #[test]
612 fn test_variable_ref() {
613 let result = engine().explain("let $x = name in $x").unwrap();
614 assert!(
616 result
617 .steps
618 .iter()
619 .any(|s| contains_node_type(s, "variable_ref"))
620 );
621 }
622
623 #[test]
624 fn test_let_expression() {
625 let result = engine().explain("let $x = name in upper($x)").unwrap();
626 let top = &result.steps[0];
627 assert_eq!(top.node_type, "let");
628 assert!(top.description.contains("1 variable"));
629 assert_eq!(top.children.len(), 2);
631 assert!(result.functions_used.contains(&"upper".to_string()));
632 }
633
634 #[test]
635 fn test_expref() {
636 let result = engine().explain("sort_by(users, &age)").unwrap();
637 assert!(
638 result
639 .steps
640 .iter()
641 .any(|s| contains_node_type(s, "expression_reference"))
642 );
643 assert!(result.functions_used.contains(&"sort_by".to_string()));
644 }
645
646 #[test]
647 fn test_object_values() {
648 let result = engine().explain("*").unwrap();
650 assert!(
651 result
652 .steps
653 .iter()
654 .any(|s| contains_node_type(s, "object_values"))
655 );
656 }
657
658 #[test]
659 fn test_not_expression() {
660 let result = engine().explain("!active").unwrap();
661 assert_eq!(result.steps[0].node_type, "not");
662 assert!(result.steps[0].description.contains("NOT"));
663 assert_eq!(result.steps[0].children.len(), 1);
664 }
665
666 #[test]
667 fn test_and_expression() {
668 let result = engine().explain("a && b").unwrap();
669 assert_eq!(result.steps[0].node_type, "and");
670 assert!(result.steps[0].description.contains("AND"));
671 assert_eq!(result.steps[0].children.len(), 2);
672 assert_eq!(result.steps[0].children[0].node_type, "field");
673 assert_eq!(result.steps[0].children[1].node_type, "field");
674 }
675
676 #[test]
677 fn test_or_expression() {
678 let result = engine().explain("a || b").unwrap();
679 assert_eq!(result.steps[0].node_type, "or");
680 assert!(result.steps[0].description.contains("OR"));
681 assert_eq!(result.steps[0].children.len(), 2);
682 assert_eq!(result.steps[0].children[0].node_type, "field");
683 assert_eq!(result.steps[0].children[1].node_type, "field");
684 }
685
686 #[test]
689 fn test_has_let_nodes_simple_field() {
690 let ast = jpx_core::parse("foo.bar").unwrap();
691 assert!(!has_let_nodes(&ast));
692 }
693
694 #[test]
695 fn test_has_let_nodes_with_let() {
696 let ast = jpx_core::parse("let $x = name in $x").unwrap();
697 assert!(has_let_nodes(&ast));
698 }
699
700 #[test]
701 fn test_has_let_nodes_nested_in_function() {
702 let ast = jpx_core::parse("length(people)").unwrap();
703 assert!(!has_let_nodes(&ast));
704 }
705
706 #[test]
707 fn test_has_let_nodes_variable_in_filter() {
708 let ast = jpx_core::parse("let $min = `30` in people[?age > $min]").unwrap();
709 assert!(has_let_nodes(&ast));
710 }
711
712 #[test]
715 fn test_complexity_simple_field() {
716 let result = engine().explain("name").unwrap();
717 assert_eq!(result.complexity, "simple");
718 }
719
720 #[test]
721 fn test_complexity_simple_identity() {
722 let result = engine().explain("@").unwrap();
723 assert_eq!(result.complexity, "simple");
724 }
725
726 #[test]
727 fn test_complexity_moderate_with_function() {
728 let result = engine().explain("length(@)").unwrap();
729 assert_eq!(result.complexity, "moderate");
731 assert!(result.functions_used.contains(&"length".to_string()));
732 }
733
734 #[test]
735 fn test_complexity_moderate_with_filter() {
736 let result = engine().explain("users[?active]").unwrap();
737 assert_eq!(result.complexity, "moderate");
739 }
740
741 #[test]
742 fn test_complexity_complex_multi_function() {
743 let result = engine().explain("sort(keys(@)) | join(', ', @)").unwrap();
745 assert_eq!(result.functions_used.len(), 3);
746 assert_eq!(result.complexity, "complex");
747 }
748
749 #[test]
752 fn test_explain_comparison_operators() {
753 let result = engine().explain("a > b").unwrap();
754 assert_eq!(result.steps[0].node_type, "comparison");
755 assert!(result.steps[0].description.contains(">"));
756 assert_eq!(result.steps[0].children.len(), 2);
757 }
758
759 #[test]
760 fn test_explain_slice() {
761 let result = engine().explain("items[1:3]").unwrap();
762 assert!(result.steps.iter().any(|s| contains_node_type(s, "slice")));
763 }
764
765 #[test]
766 fn test_explain_literal() {
767 let result = engine().explain("`42`").unwrap();
768 assert_eq!(result.steps[0].node_type, "literal");
769 assert!(result.steps[0].description.contains("42"));
770 }
771
772 #[test]
775 fn test_collect_function_names_empty() {
776 let ast = jpx_core::parse("foo.bar").unwrap();
777 assert!(collect_function_names(&ast).is_empty());
778 }
779
780 #[test]
781 fn test_collect_function_names_standard() {
782 let ast = jpx_core::parse("length(sort(@))").unwrap();
783 let names = collect_function_names(&ast);
784 assert_eq!(names, vec!["length", "sort"]);
785 }
786
787 #[test]
788 fn test_collect_function_names_nested() {
789 let ast = jpx_core::parse("users[?contains(name, 'a')] | sort_by(@, &age) | [0]").unwrap();
790 let names = collect_function_names(&ast);
791 assert_eq!(names, vec!["contains", "sort_by"]);
792 }
793}