use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::Logic;
use crate::node::CompiledNode;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PathStep {
pub node_id: u32,
pub operator: Option<String>,
pub arg_index: Option<u32>,
pub json_pointer: String,
}
struct NodeInfo {
operator: Option<String>,
arg_index: Option<u32>,
json_pointer: String,
}
impl Logic {
pub fn resolve_node_ids(&self, ids: &[u32]) -> Vec<PathStep> {
if ids.is_empty() {
return Vec::new();
}
let mut index: HashMap<u32, NodeInfo> = HashMap::new();
walk(&self.root, None, String::new(), &mut index);
let mut out = Vec::with_capacity(ids.len());
for &id in ids.iter().rev() {
if let Some(ni) = index.get(&id) {
out.push(PathStep {
node_id: id,
operator: ni.operator.clone(),
arg_index: ni.arg_index,
json_pointer: self
.pointer(id)
.map_or_else(|| ni.json_pointer.clone(), str::to_string),
});
}
}
out
}
}
fn walk(
node: &CompiledNode,
arg_index: Option<u32>,
json_pointer: String,
out: &mut HashMap<u32, NodeInfo>,
) {
if let CompiledNode::Cse(data) = node {
return walk(&data.inner, arg_index, json_pointer, out);
}
let id = node.id();
let operator = node.operator_name().map(|c| c.into_owned());
let child_parent_op = if matches!(node, CompiledNode::Array { .. }) {
None
} else {
operator.as_deref()
};
match node {
#[cfg(feature = "templating")]
CompiledNode::StructuredObject(data) => {
for (i, (key, child)) in data.fields.iter().enumerate() {
let mut pointer = json_pointer.clone();
crate::node::push_pointer_token(&mut pointer, key);
walk(child, Some(i as u32), pointer, out);
}
}
_ => node.visit_indexed_children(&mut |i, child| {
let pointer = build_pointer(&json_pointer, child_parent_op, i);
walk(child, Some(i), pointer, out);
}),
}
out.insert(
id,
NodeInfo {
operator,
arg_index,
json_pointer,
},
);
}
#[inline]
fn build_pointer(parent_pointer: &str, parent_op: Option<&str>, idx: u32) -> String {
let mut pointer = parent_pointer.to_string();
if let Some(op) = parent_op {
crate::node::push_pointer_token(&mut pointer, op);
}
pointer.push('/');
pointer.push_str(itoa::Buffer::new().format(idx));
pointer
}
#[cfg(test)]
mod tests {
fn engine() -> crate::Engine {
crate::Engine::new()
}
#[test]
fn resolve_root_only() {
let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
let root_id = compiled.root.id();
let steps = compiled.resolve_node_ids(&[root_id]);
assert_eq!(steps.len(), 1);
assert_eq!(steps[0].node_id, root_id);
assert_eq!(steps[0].operator.as_deref(), Some("=="));
assert_eq!(steps[0].arg_index, None);
assert_eq!(steps[0].json_pointer, "");
}
#[test]
fn resolve_empty_path_returns_empty() {
let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
assert!(compiled.resolve_node_ids(&[]).is_empty());
}
#[test]
fn resolve_unknown_ids_are_skipped() {
let compiled = engine().compile(r#"{"==": [{"var": "x"}, 1]}"#).unwrap();
assert!(compiled.resolve_node_ids(&[u32::MAX]).is_empty());
}
#[test]
fn resolve_via_evaluation_error() {
let engine = engine();
let compiled = engine.compile(r#"{"+": ["x", 1]}"#).unwrap();
let arena = bumpalo::Bump::new();
let data = datavalue::DataValue::from_str("null", &arena).unwrap();
let err = engine.evaluate(&compiled, data, &arena).unwrap_err();
let steps = err.resolve_path(&compiled);
assert!(
!steps.is_empty(),
"expected resolved path for arithmetic failure, got {:?}",
err
);
assert_eq!(steps[0].operator.as_deref(), Some("+"));
assert_eq!(steps[0].json_pointer, "");
}
#[test]
fn pointers_escape_and_follow_the_traced_compile() {
let engine = engine();
let rule = r#"{"/": [{"+": [{"var": "x"}, 1]}, 2]}"#;
let steps = |compiled: &crate::Logic| {
let arena = bumpalo::Bump::new();
let err = engine
.evaluate(compiled, r#"{"x": "a"}"#, &arena)
.unwrap_err();
err.resolve_path(compiled)
};
let plain = engine.compile(rule).unwrap();
let got: Vec<_> = steps(&plain).into_iter().map(|s| s.json_pointer).collect();
assert_eq!(got, ["", "/~1/0"]);
#[cfg(feature = "trace")]
{
let traced = engine.trace().compile(rule).unwrap();
let traced_steps = steps(&traced);
assert_eq!(traced_steps.last().unwrap().json_pointer, "/~1/0");
for step in &traced_steps {
assert_eq!(
traced.pointer(step.node_id),
Some(step.json_pointer.as_str())
);
}
}
}
fn pointers(rule: &str, templating: bool) -> Vec<(Option<u32>, String)> {
let engine = crate::Engine::builder()
.with_constant_folding(false)
.with_templating(templating)
.build();
let compiled = engine.compile(rule).unwrap();
let mut index = std::collections::HashMap::new();
super::walk(&compiled.root, None, String::new(), &mut index);
let mut out: Vec<_> = index
.into_values()
.map(|n| (n.arg_index, n.json_pointer))
.collect();
out.sort_by(|a, b| a.1.cmp(&b.1));
out
}
#[test]
fn var_default_is_item_one() {
let got = pointers(r#"{"var": ["x", {"+": [1, {"var": "y"}]}]}"#, false);
let ptrs: Vec<_> = got.iter().map(|(i, p)| (*i, p.as_str())).collect();
assert_eq!(
ptrs,
[
(None, ""),
(Some(1), "/var/1"),
(Some(0), "/var/1/+/0"),
(Some(1), "/var/1/+/1"),
]
);
}
#[cfg(feature = "templating")]
#[test]
fn template_fields_are_keyed() {
let got = pointers(r#"{"a": 1, "b/c": {"var": "x"}}"#, true);
let ptrs: Vec<_> = got.iter().map(|(i, p)| (*i, p.as_str())).collect();
assert_eq!(ptrs, [(None, ""), (Some(0), "/a"), (Some(1), "/b~1c")]);
}
}