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