use crate::Document;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
Key(String),
NestedKey(String),
Index(usize),
NodeId(String),
}
impl PathSegment {
pub fn as_str(&self) -> String {
match self {
PathSegment::Key(s) | PathSegment::NestedKey(s) | PathSegment::NodeId(s) => s.clone(),
PathSegment::Index(i) => format!("[{}]", i),
}
}
}
#[derive(Debug)]
pub struct VisitorContext<'a> {
pub depth: usize,
pub path: Vec<PathSegment>,
pub document: &'a Document,
pub current_schema: Option<&'a [String]>,
metadata: HashMap<String, String>,
stats: TraversalStats,
}
#[derive(Debug, Clone, Default)]
pub struct TraversalStats {
pub nodes_visited: usize,
pub scalars_visited: usize,
pub lists_visited: usize,
pub objects_visited: usize,
pub max_depth_reached: usize,
}
impl<'a> VisitorContext<'a> {
pub fn new(document: &'a Document) -> Self {
Self {
depth: 0,
path: Vec::new(),
document,
current_schema: None,
metadata: HashMap::new(),
stats: TraversalStats::default(),
}
}
pub fn child(&self, segment: PathSegment) -> Self {
let mut path = self.path.clone();
path.push(segment);
Self {
depth: self.depth + 1,
path,
document: self.document,
current_schema: self.current_schema,
metadata: self.metadata.clone(),
stats: self.stats.clone(),
}
}
pub fn with_schema(&self, schema: &'a [String]) -> Self {
Self {
depth: self.depth,
path: self.path.clone(),
document: self.document,
current_schema: Some(schema),
metadata: self.metadata.clone(),
stats: self.stats.clone(),
}
}
pub fn path_string(&self) -> String {
if self.path.is_empty() {
"root".to_string()
} else {
self.path
.iter()
.map(|seg| seg.as_str())
.collect::<Vec<_>>()
.join(".")
}
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.metadata.insert(key.into(), value.into());
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(|s| s.as_str())
}
pub fn stats(&self) -> &TraversalStats {
&self.stats
}
pub(crate) fn record_node_visit(&mut self) {
self.stats.nodes_visited += 1;
self.stats.max_depth_reached = self.stats.max_depth_reached.max(self.depth);
}
pub(crate) fn record_scalar_visit(&mut self) {
self.stats.scalars_visited += 1;
self.stats.max_depth_reached = self.stats.max_depth_reached.max(self.depth);
}
pub(crate) fn record_list_visit(&mut self) {
self.stats.lists_visited += 1;
}
pub(crate) fn record_object_visit(&mut self) {
self.stats.objects_visited += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_context() {
let doc = Document::new((2, 0));
let ctx = VisitorContext::new(&doc);
assert_eq!(ctx.depth, 0);
assert!(ctx.path.is_empty());
assert_eq!(ctx.path_string(), "root");
assert!(ctx.current_schema.is_none());
}
#[test]
fn test_child_context_increments_depth() {
let doc = Document::new((2, 0));
let ctx = VisitorContext::new(&doc);
let child = ctx.child(PathSegment::Key("users".to_string()));
assert_eq!(child.depth, 1);
assert_eq!(child.path.len(), 1);
}
#[test]
fn test_path_string_with_nested_keys() {
let doc = Document::new((2, 0));
let ctx = VisitorContext::new(&doc);
let ctx = ctx.child(PathSegment::Key("a".to_string()));
let ctx = ctx.child(PathSegment::NestedKey("b".to_string()));
let ctx = ctx.child(PathSegment::Key("c".to_string()));
assert_eq!(ctx.path_string(), "a.b.c");
}
#[test]
fn test_path_string_with_index() {
let doc = Document::new((2, 0));
let ctx = VisitorContext::new(&doc);
let ctx = ctx.child(PathSegment::Key("users".to_string()));
let ctx = ctx.child(PathSegment::Index(0));
assert_eq!(ctx.path_string(), "users.[0]");
}
#[test]
fn test_with_schema() {
let doc = Document::new((2, 0));
let ctx = VisitorContext::new(&doc);
let schema = vec!["id".to_string(), "name".to_string()];
let ctx_with_schema = ctx.with_schema(&schema);
assert!(ctx_with_schema.current_schema.is_some());
assert_eq!(ctx_with_schema.current_schema.unwrap().len(), 2);
assert_eq!(ctx_with_schema.depth, ctx.depth);
}
#[test]
fn test_metadata_storage() {
let doc = Document::new((2, 0));
let mut ctx = VisitorContext::new(&doc);
ctx.set_metadata("key", "value");
assert_eq!(ctx.get_metadata("key"), Some("value"));
assert_eq!(ctx.get_metadata("missing"), None);
}
#[test]
fn test_metadata_persists_in_child() {
let doc = Document::new((2, 0));
let mut ctx = VisitorContext::new(&doc);
ctx.set_metadata("key", "value");
let child = ctx.child(PathSegment::Key("child".to_string()));
assert_eq!(child.get_metadata("key"), Some("value"));
}
#[test]
fn test_stats_tracking() {
let doc = Document::new((2, 0));
let mut ctx = VisitorContext::new(&doc);
ctx.record_node_visit();
ctx.record_scalar_visit();
ctx.record_list_visit();
ctx.record_object_visit();
let stats = ctx.stats();
assert_eq!(stats.nodes_visited, 1);
assert_eq!(stats.scalars_visited, 1);
assert_eq!(stats.lists_visited, 1);
assert_eq!(stats.objects_visited, 1);
}
#[test]
fn test_stats_max_depth() {
let doc = Document::new((2, 0));
let mut ctx = VisitorContext::new(&doc);
ctx.record_node_visit();
assert_eq!(ctx.stats().max_depth_reached, 0);
let mut child = ctx.child(PathSegment::Key("a".to_string()));
child.record_node_visit();
assert_eq!(child.stats().max_depth_reached, 1);
}
#[test]
fn test_path_segment_as_str() {
assert_eq!(PathSegment::Key("test".to_string()).as_str(), "test");
assert_eq!(
PathSegment::NestedKey("nested".to_string()).as_str(),
"nested"
);
assert_eq!(PathSegment::Index(5).as_str(), "[5]");
assert_eq!(PathSegment::NodeId("id123".to_string()).as_str(), "id123");
}
#[test]
fn test_stats_default() {
let stats = TraversalStats::default();
assert_eq!(stats.nodes_visited, 0);
assert_eq!(stats.scalars_visited, 0);
assert_eq!(stats.lists_visited, 0);
assert_eq!(stats.objects_visited, 0);
assert_eq!(stats.max_depth_reached, 0);
}
#[test]
fn test_stats_clone() {
let stats = TraversalStats {
nodes_visited: 5,
..Default::default()
};
let cloned = stats.clone();
assert_eq!(cloned.nodes_visited, 5);
}
}