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,
40}
41
42struct NodeInfo {
44 operator: Option<String>,
45 arg_index: Option<u32>,
46 json_pointer: String,
47}
48
49impl Logic {
50 pub fn resolve_node_ids(&self, ids: &[u32]) -> Vec<PathStep> {
58 if ids.is_empty() {
59 return Vec::new();
60 }
61
62 let mut index: HashMap<u32, NodeInfo> = HashMap::new();
63 walk(&self.root, None, String::new(), &mut index);
64
65 let mut out = Vec::with_capacity(ids.len());
66 for &id in ids.iter().rev() {
68 if let Some(ni) = index.get(&id) {
69 out.push(PathStep {
70 node_id: id,
71 operator: ni.operator.clone(),
72 arg_index: ni.arg_index,
73 json_pointer: self
74 .pointer(id)
75 .map_or_else(|| ni.json_pointer.clone(), str::to_string),
76 });
77 }
78 }
79 out
80 }
81}
82
83fn walk(
96 node: &CompiledNode,
97 arg_index: Option<u32>,
98 json_pointer: String,
99 out: &mut HashMap<u32, NodeInfo>,
100) {
101 if let CompiledNode::Cse(data) = node {
105 return walk(&data.inner, arg_index, json_pointer, out);
106 }
107
108 let id = node.id();
109 let operator = node.operator_name().map(|c| c.into_owned());
110
111 let child_parent_op = if matches!(node, CompiledNode::Array { .. }) {
114 None
115 } else {
116 operator.as_deref()
117 };
118
119 match node {
123 #[cfg(feature = "templating")]
125 CompiledNode::StructuredObject(data) => {
126 for (i, (key, child)) in data.fields.iter().enumerate() {
127 let mut pointer = json_pointer.clone();
128 crate::node::push_pointer_token(&mut pointer, key);
129 walk(child, Some(i as u32), pointer, out);
130 }
131 }
132 _ => node.visit_indexed_children(&mut |i, child| {
133 let pointer = build_pointer(&json_pointer, child_parent_op, i);
134 walk(child, Some(i), pointer, out);
135 }),
136 }
137
138 out.insert(
139 id,
140 NodeInfo {
141 operator,
142 arg_index,
143 json_pointer,
144 },
145 );
146}
147
148#[inline]
151fn build_pointer(parent_pointer: &str, parent_op: Option<&str>, idx: u32) -> String {
152 let mut pointer = parent_pointer.to_string();
153 if let Some(op) = parent_op {
154 crate::node::push_pointer_token(&mut pointer, op);
155 }
156 pointer.push('/');
157 pointer.push_str(itoa::Buffer::new().format(idx));
158 pointer
159}
160
161#[cfg(test)]
162mod tests {
163 fn engine() -> crate::Engine {
164 crate::Engine::new()
165 }
166
167 #[test]
168 fn resolve_root_only() {
169 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
171 let root_id = compiled.root.id();
172 let steps = compiled.resolve_node_ids(&[root_id]);
173 assert_eq!(steps.len(), 1);
174 assert_eq!(steps[0].node_id, root_id);
175 assert_eq!(steps[0].operator.as_deref(), Some("=="));
176 assert_eq!(steps[0].arg_index, None);
177 assert_eq!(steps[0].json_pointer, "");
178 }
179
180 #[test]
181 fn resolve_empty_path_returns_empty() {
182 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
183 assert!(compiled.resolve_node_ids(&[]).is_empty());
184 }
185
186 #[test]
187 fn resolve_unknown_ids_are_skipped() {
188 let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
189 assert!(compiled.resolve_node_ids(&[u32::MAX]).is_empty());
191 }
192
193 #[test]
194 fn resolve_via_evaluation_error() {
195 let engine = engine();
197 let compiled = engine.compile(r#"{"+": ["x", 1]}"#).unwrap();
198 let arena = bumpalo::Bump::new();
199 let data = datavalue::DataValue::from_str("null", &arena).unwrap();
200 let err = engine.evaluate(&compiled, data, &arena).unwrap_err();
201 let steps = err.resolve_path(&compiled);
203 assert!(
204 !steps.is_empty(),
205 "expected resolved path for arithmetic failure, got {:?}",
206 err
207 );
208 assert_eq!(steps[0].operator.as_deref(), Some("+"));
210 assert_eq!(steps[0].json_pointer, "");
211 }
212
213 #[test]
216 fn pointers_escape_and_follow_the_traced_compile() {
217 let engine = engine();
218 let rule = r#"{"/": [{"+": [{"var": "x"}, 1]}, 2]}"#;
219 let steps = |compiled: &crate::Logic| {
220 let arena = bumpalo::Bump::new();
221 let err = engine
222 .evaluate(compiled, r#"{"x": "a"}"#, &arena)
223 .unwrap_err();
224 err.resolve_path(compiled)
225 };
226 let plain = engine.compile(rule).unwrap();
227 let got: Vec<_> = steps(&plain).into_iter().map(|s| s.json_pointer).collect();
228 assert_eq!(got, ["", "/~1/0"]);
229 #[cfg(feature = "trace")]
230 {
231 let traced = engine.trace().compile(rule).unwrap();
232 let traced_steps = steps(&traced);
233 assert_eq!(traced_steps.last().unwrap().json_pointer, "/~1/0");
234 for step in &traced_steps {
235 assert_eq!(
236 traced.pointer(step.node_id),
237 Some(step.json_pointer.as_str())
238 );
239 }
240 }
241 }
242
243 fn pointers(rule: &str, templating: bool) -> Vec<(Option<u32>, String)> {
245 let engine = crate::Engine::builder()
246 .with_constant_folding(false)
247 .with_templating(templating)
248 .build();
249 let compiled = engine.compile(rule).unwrap();
250 let mut index = std::collections::HashMap::new();
251 super::walk(&compiled.root, None, String::new(), &mut index);
252 let mut out: Vec<_> = index
253 .into_values()
254 .map(|n| (n.arg_index, n.json_pointer))
255 .collect();
256 out.sort_by(|a, b| a.1.cmp(&b.1));
257 out
258 }
259
260 #[test]
262 fn var_default_is_item_one() {
263 let got = pointers(r#"{"var": ["x", {"+": [1, {"var": "y"}]}]}"#, false);
264 let ptrs: Vec<_> = got.iter().map(|(i, p)| (*i, p.as_str())).collect();
265 assert_eq!(
266 ptrs,
267 [
268 (None, ""),
269 (Some(1), "/var/1"),
270 (Some(0), "/var/1/+/0"),
271 (Some(1), "/var/1/+/1"),
272 ]
273 );
274 }
275
276 #[cfg(feature = "templating")]
278 #[test]
279 fn template_fields_are_keyed() {
280 let got = pointers(r#"{"a": 1, "b/c": {"var": "x"}}"#, true);
281 let ptrs: Vec<_> = got.iter().map(|(i, p)| (*i, p.as_str())).collect();
282 assert_eq!(ptrs, [(None, ""), (Some(0), "/a"), (Some(1), "/b~1c")]);
283 }
284}