datalogic_rs/context.rs
1use serde_json::Value;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5// Static string constants for common metadata keys
6pub const INDEX_KEY: &str = "index";
7pub const KEY_KEY: &str = "key";
8pub const CURRENT_KEY: &str = "current";
9pub const ACCUMULATOR_KEY: &str = "accumulator";
10
11/// A single frame in the context stack (for temporary/nested contexts)
12pub struct ContextFrame {
13 /// The data value at this context level
14 pub data: Value,
15 /// Optional metadata for this frame (e.g., "index", "key" in map operations)
16 pub metadata: Option<HashMap<String, Value>>,
17}
18
19/// Reference to a context frame (either actual frame or root)
20pub enum ContextFrameRef<'a> {
21 /// Reference to an actual frame
22 Frame(&'a ContextFrame),
23 /// Reference to the root data
24 Root(&'a Arc<Value>),
25}
26
27impl<'a> ContextFrameRef<'a> {
28 /// Get the data value
29 pub fn data(&self) -> &Value {
30 match self {
31 ContextFrameRef::Frame(frame) => &frame.data,
32 ContextFrameRef::Root(root) => root,
33 }
34 }
35
36 /// Get the metadata (only available for frames)
37 pub fn metadata(&self) -> Option<&HashMap<String, Value>> {
38 match self {
39 ContextFrameRef::Frame(frame) => frame.metadata.as_ref(),
40 ContextFrameRef::Root(_) => None,
41 }
42 }
43}
44
45/// Context stack for nested evaluations
46pub struct ContextStack {
47 /// Immutable root data (never changes during evaluation)
48 root: Arc<Value>,
49 /// Stack of temporary frames (excludes root)
50 frames: Vec<ContextFrame>,
51}
52
53impl ContextStack {
54 /// Create a new context stack with Arc root data
55 pub fn new(root: Arc<Value>) -> Self {
56 Self {
57 root,
58 frames: Vec::new(),
59 }
60 }
61
62 /// Pushes a new context frame for nested evaluation.
63 ///
64 /// Used by operators that need to create a nested data context without metadata.
65 ///
66 /// # Arguments
67 ///
68 /// * `data` - The data value for the new context frame
69 pub fn push(&mut self, data: Value) {
70 self.frames.push(ContextFrame {
71 data,
72 metadata: None,
73 });
74 }
75
76 /// Pushes a frame with metadata for iteration operations.
77 ///
78 /// Used by array operators like `map`, `filter`, and `reduce` to provide
79 /// iteration context including index and key information.
80 ///
81 /// # Arguments
82 ///
83 /// * `data` - The current item being processed
84 /// * `metadata` - Iteration metadata (typically includes "index" and optionally "key")
85 ///
86 /// # Example
87 ///
88 /// During array iteration:
89 /// ```rust,ignore
90 /// let mut metadata = HashMap::new();
91 /// metadata.insert("index".to_string(), json!(0));
92 /// context.push_with_metadata(item_value, metadata);
93 /// ```
94 pub fn push_with_metadata(&mut self, data: Value, metadata: HashMap<String, Value>) {
95 self.frames.push(ContextFrame {
96 data,
97 metadata: Some(metadata),
98 });
99 }
100
101 /// Pops the current context frame from the stack.
102 ///
103 /// Restores the previous context after nested evaluation completes.
104 /// Returns None if there are no frames to pop (root is never popped).
105 ///
106 /// # Returns
107 ///
108 /// The popped context frame, or None if the stack is empty.
109 pub fn pop(&mut self) -> Option<ContextFrame> {
110 // Only pop if there are frames (root is separate)
111 self.frames.pop()
112 }
113
114 /// Accesses data at a context level relative to current.
115 ///
116 /// This method enables access to parent contexts during nested operations,
117 /// which is essential for complex JSONLogic expressions.
118 ///
119 /// # Arguments
120 ///
121 /// * `level` - The number of levels to traverse up the context stack
122 /// - 0: returns the current context
123 /// - N (positive or negative): goes up N levels from current
124 ///
125 /// # Returns
126 ///
127 /// A reference to the context frame at the specified level,
128 /// or None if the level exceeds the stack depth.
129 ///
130 /// # Note
131 ///
132 /// The sign of the level is ignored; both positive and negative values
133 /// traverse up the stack the same way. This maintains backward compatibility
134 /// with existing usage patterns.
135 ///
136 /// # Returns
137 /// The context frame at the specified level, or the root if level exceeds stack depth
138 pub fn get_at_level(&self, level: isize) -> Option<ContextFrameRef<'_>> {
139 // Get absolute value - sign doesn't matter (for backward compatibility)
140 let levels_up = level.unsigned_abs();
141
142 if levels_up == 0 {
143 // 0 means current context
144 return Some(self.current());
145 }
146
147 let frame_count = self.frames.len();
148
149 // If going up more levels than or equal to the total frame count,
150 // we reach the root (since root is not in frames)
151 if levels_up >= frame_count {
152 return Some(ContextFrameRef::Root(&self.root));
153 }
154
155 // Calculate target index by going up from current
156 let target_index = frame_count - levels_up;
157 self.frames.get(target_index).map(ContextFrameRef::Frame)
158 }
159
160 /// Get the current context frame (top of stack)
161 /// Returns a temporary frame for root if no frames are pushed
162 pub fn current(&self) -> ContextFrameRef<'_> {
163 if let Some(frame) = self.frames.last() {
164 ContextFrameRef::Frame(frame)
165 } else {
166 ContextFrameRef::Root(&self.root)
167 }
168 }
169
170 /// Get the root context frame
171 pub fn root(&self) -> ContextFrameRef<'_> {
172 ContextFrameRef::Root(&self.root)
173 }
174
175 /// Get the current depth (number of frames)
176 pub fn depth(&self) -> usize {
177 self.frames.len()
178 }
179}