1use std::collections::HashMap;
6
7use serde::{Deserialize, Serialize};
8
9use crate::Logic;
10use crate::node::CompiledNode;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
22#[non_exhaustive]
23pub struct PathStep {
24 pub node_id: u32,
26 pub operator: Option<String>,
29 pub arg_index: Option<u32>,
32 pub json_pointer: String,
36}
37
38struct NodeInfo {
40 operator: Option<String>,
41 arg_index: Option<u32>,
42 json_pointer: String,
43}
44
45impl Logic {
46 pub fn resolve_node_ids(&self, ids: &[u32]) -> Vec<PathStep> {
54 if ids.is_empty() {
55 return Vec::new();
56 }
57
58 let mut index: HashMap<u32, NodeInfo> = HashMap::new();
59 walk(&self.root, None, None, "", &mut index);
60
61 let mut out = Vec::with_capacity(ids.len());
62 for &id in ids.iter().rev() {
64 if let Some(ni) = index.get(&id) {
65 out.push(PathStep {
66 node_id: id,
67 operator: ni.operator.clone(),
68 arg_index: ni.arg_index,
69 json_pointer: ni.json_pointer.clone(),
70 });
71 }
72 }
73 out
74 }
75}
76
77fn walk(
85 node: &CompiledNode,
86 parent_op: Option<&str>,
87 arg_index: Option<u32>,
88 parent_pointer: &str,
89 out: &mut HashMap<u32, NodeInfo>,
90) {
91 let id = node.id();
92 let operator = node.operator_name();
93 let json_pointer = build_pointer(parent_pointer, parent_op, arg_index);
94 out.insert(
95 id,
96 NodeInfo {
97 operator: operator.clone(),
98 arg_index,
99 json_pointer: json_pointer.clone(),
100 },
101 );
102
103 let child_parent_op = if matches!(node, CompiledNode::Array { .. }) {
106 None
107 } else {
108 operator.as_deref()
109 };
110
111 node.visit_indexed_children(&mut |i, child| {
112 walk(child, child_parent_op, Some(i), &json_pointer, out);
113 });
114}
115
116#[inline]
117fn build_pointer(parent_pointer: &str, parent_op: Option<&str>, arg_index: Option<u32>) -> String {
118 match (parent_op, arg_index) {
119 (Some(op), Some(idx)) => format!("{}/{}/{}", parent_pointer, op, idx),
120 (None, Some(idx)) => format!("{}/{}", parent_pointer, idx),
122 _ => parent_pointer.to_string(),
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 fn engine() -> crate::Engine {
129 crate::Engine::new()
130 }
131
132 #[test]
133 fn resolve_root_only() {
134 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
136 let root_id = compiled.root.id();
137 let steps = compiled.resolve_node_ids(&[root_id]);
138 assert_eq!(steps.len(), 1);
139 assert_eq!(steps[0].node_id, root_id);
140 assert_eq!(steps[0].operator.as_deref(), Some("=="));
141 assert_eq!(steps[0].arg_index, None);
142 assert_eq!(steps[0].json_pointer, "");
143 }
144
145 #[test]
146 fn resolve_empty_path_returns_empty() {
147 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
148 assert!(compiled.resolve_node_ids(&[]).is_empty());
149 }
150
151 #[test]
152 fn resolve_unknown_ids_are_skipped() {
153 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
154 assert!(compiled.resolve_node_ids(&[u32::MAX]).is_empty());
156 }
157
158 #[test]
159 fn resolve_via_evaluation_error() {
160 let engine = engine();
162 let compiled = engine.compile(r#"{"+": ["x", 1]}"#).unwrap();
163 let arena = bumpalo::Bump::new();
164 let data = datavalue::DataValue::from_str("null", &arena).unwrap();
165 let err = engine.evaluate(&compiled, data, &arena).unwrap_err();
166 let steps = err.resolve_path(&compiled);
168 assert!(
169 !steps.is_empty(),
170 "expected resolved path for arithmetic failure, got {:?}",
171 err
172 );
173 assert_eq!(steps[0].operator.as_deref(), Some("+"));
175 assert_eq!(steps[0].json_pointer, "");
176 }
177}