use std::collections::HashMap;
use super::ops::{CompiledOp, OpKind};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum PathToken {
Key(String),
Index(usize),
}
pub(super) fn path_eq_tokens(path: &[PathToken], tokens: &[String]) -> bool {
if path.len() != tokens.len() {
return false;
}
path.iter().zip(tokens).all(|(segment, token)| match segment {
PathToken::Key(key) => key == token,
PathToken::Index(idx) => array_index(token) == Some(*idx),
})
}
type NodeId = u32;
#[derive(Clone, Debug, Default)]
struct PathNode {
nested: bool,
extract_idx: Option<u32>,
mutate_idx: Option<u32>,
extract_branch: bool,
object_children: HashMap<String, NodeId>,
array_children: HashMap<usize, NodeId>,
array_append: Option<u32>,
}
#[derive(Clone, Debug)]
pub(super) struct OpPathIndex {
nodes: Vec<PathNode>,
miss: NodeId,
empty: PathNode,
}
pub(super) fn array_index(token: &str) -> Option<usize> {
if token.is_empty() || (token.starts_with('0') && token.len() > 1) {
return None;
}
if !token.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
token.parse().ok()
}
fn nid(id: NodeId) -> Option<usize> {
usize::try_from(id).ok()
}
impl OpPathIndex {
#[expect(clippy::too_many_lines, reason = "trie construction is a linear walk over ops")]
pub(super) fn build(ops: &[CompiledOp]) -> Self {
let mut nodes = vec![PathNode::default()];
for (idx, op) in ops.iter().enumerate() {
let Some(idx_u32) = u32::try_from(idx).ok() else {
continue;
};
if op.tokens.is_empty() {
if let Some(node) = nodes.get_mut(0) {
if op.kind == OpKind::Extract {
node.extract_idx = Some(idx_u32);
node.extract_branch = true;
} else if op.kind.is_mutating() {
node.mutate_idx = Some(idx_u32);
}
}
continue;
}
let mut node_id = 0_u32;
for (depth, token) in op.tokens.iter().enumerate() {
let is_last = depth.saturating_add(1) == op.tokens.len();
if token == "-" && is_last && op.kind == OpKind::Add {
if let Some(node) = nid(node_id).and_then(|i| nodes.get_mut(i)) {
node.array_append = Some(idx_u32);
}
break;
}
let parent_idx = node_id;
node_id = ensure_child(&mut nodes, parent_idx, token);
if let Some(parent) = nid(parent_idx).and_then(|i| nodes.get_mut(i)) {
if !is_last {
parent.nested = true;
}
if op.kind == OpKind::Extract {
parent.extract_branch = true;
}
}
if is_last && let Some(node) = nid(node_id).and_then(|i| nodes.get_mut(i)) {
match op.kind {
OpKind::Extract => node.extract_idx = Some(idx_u32),
OpKind::Add | OpKind::Replace | OpKind::Remove => node.mutate_idx = Some(idx_u32),
}
}
}
}
let miss = u32::try_from(nodes.len()).unwrap_or(0);
nodes.push(PathNode::default());
Self {
nodes,
miss,
empty: PathNode::default(),
}
}
fn node(&self, id: NodeId) -> &PathNode {
nid(id)
.and_then(|i| self.nodes.get(i))
.or_else(|| self.nodes.first())
.unwrap_or(&self.empty)
}
pub(super) fn extract_branch_at(&self, path: &[PathToken]) -> bool {
let node = self.node_at(path);
node.extract_branch || node.extract_idx.is_some()
}
pub(super) fn has_descendant_extracts(&self, path: &[PathToken]) -> bool {
self.node_at(path).extract_branch
}
pub(super) fn has_descendant_ops(&self, path: &[PathToken]) -> bool {
Self::node_has_work(self.node_at(path))
}
pub(super) fn child_needs_rewrite(&self, path: &[PathToken], key: &str) -> bool {
self.child_object_node(path, key)
.is_some_and(|node_id| Self::node_has_work(self.node(node_id)))
}
pub(super) fn child_index_needs_rewrite(&self, path: &[PathToken], idx: usize) -> bool {
self.child_array_node(path, idx)
.is_some_and(|node_id| Self::node_has_work(self.node(node_id)))
}
fn node_has_work(node: &PathNode) -> bool {
node.nested
|| node.mutate_idx.is_some()
|| node.extract_idx.is_some()
|| !node.object_children.is_empty()
|| !node.array_children.is_empty()
|| node.array_append.is_some()
}
pub(super) fn extract_at(&self, path: &[PathToken]) -> Option<u32> {
self.node_at(path).extract_idx
}
pub(super) fn mutate_child_key(&self, path: &[PathToken], last: &str) -> Option<u32> {
self.child_object_node(path, last)
.and_then(|node_id| self.node(node_id).mutate_idx)
}
pub(super) fn mutate_at_index(&self, path: &[PathToken], idx: usize) -> Option<u32> {
self.child_array_node(path, idx)
.and_then(|node_id| self.node(node_id).mutate_idx)
}
pub(super) fn add_append(&self, path: &[PathToken]) -> Option<u32> {
self.node_at(path).array_append
}
fn node_at(&self, path: &[PathToken]) -> &PathNode {
let mut node_id = 0_u32;
for segment in path {
let node = self.node(node_id);
let next = match segment {
PathToken::Key(key) => node.object_children.get(key).copied(),
PathToken::Index(idx) => node.array_children.get(idx).copied(),
};
match next {
Some(id) => node_id = id,
None => return self.node(self.miss),
}
}
self.node(node_id)
}
fn child_object_node(&self, path: &[PathToken], key: &str) -> Option<NodeId> {
self.node_at(path).object_children.get(key).copied()
}
fn child_array_node(&self, path: &[PathToken], idx: usize) -> Option<NodeId> {
self.node_at(path).array_children.get(&idx).copied()
}
}
fn child_id_for_token(nodes: &[PathNode], parent_idx: u32, token: &str) -> Option<NodeId> {
let parent = nid(parent_idx).and_then(|i| nodes.get(i))?;
if let Some(idx) = array_index(token)
&& let Some(&id) = parent.array_children.get(&idx)
{
return Some(id);
}
parent.object_children.get(token).copied()
}
fn link_token(nodes: &mut [PathNode], parent_idx: u32, token: &str, child: NodeId) {
let Some(parent) = nid(parent_idx).and_then(|i| nodes.get_mut(i)) else {
return;
};
parent.object_children.entry(token.to_owned()).or_insert(child);
if let Some(idx) = array_index(token) {
parent.array_children.entry(idx).or_insert(child);
}
}
fn ensure_child(nodes: &mut Vec<PathNode>, parent_idx: u32, token: &str) -> NodeId {
if let Some(id) = child_id_for_token(nodes, parent_idx, token) {
link_token(nodes, parent_idx, token, id);
return id;
}
let child = u32::try_from(nodes.len()).unwrap_or(0);
nodes.push(PathNode::default());
link_token(nodes, parent_idx, token, child);
child
}