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 index for array iteration (avoids HashMap allocation)
16    pub index: Option<usize>,
17    /// Optional metadata for this frame (e.g., "key" in map operations)
18    /// Only used when additional metadata beyond index is needed
19    pub metadata: Option<HashMap<String, Value>>,
20}
21
22/// Reference to a context frame (either actual frame or root)
23pub enum ContextFrameRef<'a> {
24    /// Reference to an actual frame
25    Frame(&'a ContextFrame),
26    /// Reference to the root data
27    Root(&'a Arc<Value>),
28}
29
30impl<'a> ContextFrameRef<'a> {
31    /// Get the data value
32    pub fn data(&self) -> &Value {
33        match self {
34            ContextFrameRef::Frame(frame) => &frame.data,
35            ContextFrameRef::Root(root) => root,
36        }
37    }
38
39    /// Get the index if available (fast path, no HashMap lookup)
40    #[inline]
41    pub fn get_index(&self) -> Option<usize> {
42        match self {
43            ContextFrameRef::Frame(frame) => frame.index,
44            ContextFrameRef::Root(_) => None,
45        }
46    }
47
48    /// Get the metadata (only available for frames)
49    pub fn metadata(&self) -> Option<&HashMap<String, Value>> {
50        match self {
51            ContextFrameRef::Frame(frame) => frame.metadata.as_ref(),
52            ContextFrameRef::Root(_) => None,
53        }
54    }
55}
56
57/// Context stack for nested evaluations
58pub struct ContextStack {
59    /// Immutable root data (never changes during evaluation)
60    root: Arc<Value>,
61    /// Stack of temporary frames (excludes root)
62    frames: Vec<ContextFrame>,
63}
64
65impl ContextStack {
66    /// Create a new context stack with Arc root data
67    pub fn new(root: Arc<Value>) -> Self {
68        Self {
69            root,
70            frames: Vec::new(),
71        }
72    }
73
74    /// Pushes a new context frame for nested evaluation.
75    ///
76    /// Used by operators that need to create a nested data context without metadata.
77    ///
78    /// # Arguments
79    ///
80    /// * `data` - The data value for the new context frame
81    pub fn push(&mut self, data: Value) {
82        self.frames.push(ContextFrame {
83            data,
84            index: None,
85            metadata: None,
86        });
87    }
88
89    /// Pushes a frame with just an index (optimized path - no HashMap allocation).
90    ///
91    /// Used by array operators like `map` and `filter` for simple iteration
92    /// where only index access is needed.
93    ///
94    /// # Arguments
95    ///
96    /// * `data` - The current item being processed
97    /// * `index` - The array index of the current item
98    #[inline]
99    pub fn push_with_index(&mut self, data: Value, index: usize) {
100        self.frames.push(ContextFrame {
101            data,
102            index: Some(index),
103            metadata: None,
104        });
105    }
106
107    /// Pushes a frame with metadata for iteration operations.
108    ///
109    /// Used by array operators like `map`, `filter`, and `reduce` to provide
110    /// iteration context including index and key information.
111    ///
112    /// # Arguments
113    ///
114    /// * `data` - The current item being processed
115    /// * `metadata` - Iteration metadata (typically includes "index" and optionally "key")
116    ///
117    /// # Example
118    ///
119    /// During array iteration:
120    /// ```rust,ignore
121    /// let mut metadata = HashMap::new();
122    /// metadata.insert("index".to_string(), json!(0));
123    /// context.push_with_metadata(item_value, metadata);
124    /// ```
125    pub fn push_with_metadata(&mut self, data: Value, metadata: HashMap<String, Value>) {
126        // Extract index from metadata if present
127        let index = metadata
128            .get(INDEX_KEY)
129            .and_then(|v| v.as_u64())
130            .map(|i| i as usize);
131
132        self.frames.push(ContextFrame {
133            data,
134            index,
135            metadata: Some(metadata),
136        });
137    }
138
139    /// Pops the current context frame from the stack.
140    ///
141    /// Restores the previous context after nested evaluation completes.
142    /// Returns None if there are no frames to pop (root is never popped).
143    ///
144    /// # Returns
145    ///
146    /// The popped context frame, or None if the stack is empty.
147    pub fn pop(&mut self) -> Option<ContextFrame> {
148        // Only pop if there are frames (root is separate)
149        self.frames.pop()
150    }
151
152    /// Accesses data at a context level relative to current.
153    ///
154    /// This method enables access to parent contexts during nested operations,
155    /// which is essential for complex JSONLogic expressions.
156    ///
157    /// # Arguments
158    ///
159    /// * `level` - The number of levels to traverse up the context stack
160    ///   - 0: returns the current context
161    ///   - N (positive or negative): goes up N levels from current
162    ///
163    /// # Returns
164    ///
165    /// A reference to the context frame at the specified level,
166    /// or None if the level exceeds the stack depth.
167    ///
168    /// # Note
169    ///
170    /// The sign of the level is ignored; both positive and negative values
171    /// traverse up the stack the same way. This maintains backward compatibility
172    /// with existing usage patterns.
173    ///
174    /// # Returns
175    /// The context frame at the specified level, or the root if level exceeds stack depth
176    pub fn get_at_level(&self, level: isize) -> Option<ContextFrameRef<'_>> {
177        // Get absolute value - sign doesn't matter (for backward compatibility)
178        let levels_up = level.unsigned_abs();
179
180        if levels_up == 0 {
181            // 0 means current context
182            return Some(self.current());
183        }
184
185        let frame_count = self.frames.len();
186
187        // If going up more levels than or equal to the total frame count,
188        // we reach the root (since root is not in frames)
189        if levels_up >= frame_count {
190            return Some(ContextFrameRef::Root(&self.root));
191        }
192
193        // Calculate target index by going up from current
194        let target_index = frame_count - levels_up;
195        self.frames.get(target_index).map(ContextFrameRef::Frame)
196    }
197
198    /// Get the current context frame (top of stack)
199    /// Returns a temporary frame for root if no frames are pushed
200    pub fn current(&self) -> ContextFrameRef<'_> {
201        if let Some(frame) = self.frames.last() {
202            ContextFrameRef::Frame(frame)
203        } else {
204            ContextFrameRef::Root(&self.root)
205        }
206    }
207
208    /// Get the root context frame
209    pub fn root(&self) -> ContextFrameRef<'_> {
210        ContextFrameRef::Root(&self.root)
211    }
212
213    /// Get the current depth (number of frames)
214    pub fn depth(&self) -> usize {
215        self.frames.len()
216    }
217}