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, None, "", &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: ni.json_pointer.clone(),
});
}
}
out
}
}
fn walk(
node: &CompiledNode,
parent_op: Option<&str>,
arg_index: Option<u32>,
parent_pointer: &str,
out: &mut HashMap<u32, NodeInfo>,
) {
let id = node.id();
let operator = node.operator_name();
let json_pointer = build_pointer(parent_pointer, parent_op, arg_index);
out.insert(
id,
NodeInfo {
operator: operator.clone(),
arg_index,
json_pointer: json_pointer.clone(),
},
);
let child_parent_op = if matches!(node, CompiledNode::Array { .. }) {
None
} else {
operator.as_deref()
};
node.visit_indexed_children(&mut |i, child| {
walk(child, child_parent_op, Some(i), &json_pointer, out);
});
}
#[inline]
fn build_pointer(parent_pointer: &str, parent_op: Option<&str>, arg_index: Option<u32>) -> String {
match (parent_op, arg_index) {
(Some(op), Some(idx)) => format!("{}/{}/{}", parent_pointer, op, idx),
(None, Some(idx)) => format!("{}/{}", parent_pointer, idx),
_ => parent_pointer.to_string(),
}
}
#[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, "");
}
}