Skip to main content

rlm_rs/core/
context.rs

1//! RLM execution context.
2//!
3//! Represents the stateful environment for RLM operations, including
4//! variables, globals, and active buffer references.
5
6use crate::io::current_timestamp;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Represents the RLM execution context.
11///
12/// This mirrors the Python implementation's context dict and globals,
13/// providing a persistent state across operations.
14///
15/// # Examples
16///
17/// ```
18/// use rlm_rs::core::Context;
19///
20/// let mut ctx = Context::new();
21/// ctx.set_variable("key".to_string(), "value".into());
22/// assert!(ctx.get_variable("key").is_some());
23/// ```
24#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
25pub struct Context {
26    /// Context variables (key-value pairs for current session).
27    pub variables: HashMap<String, ContextValue>,
28
29    /// Global state dictionary (persisted across sessions).
30    pub globals: HashMap<String, ContextValue>,
31
32    /// Active buffer IDs in this context.
33    pub buffer_ids: Vec<i64>,
34
35    /// Current working directory path.
36    pub cwd: Option<String>,
37
38    /// Context metadata.
39    pub metadata: ContextMetadata,
40}
41
42/// Metadata associated with a context.
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub struct ContextMetadata {
45    /// Unix timestamp when context was created.
46    pub created_at: i64,
47
48    /// Unix timestamp when context was last modified.
49    pub updated_at: i64,
50
51    /// Schema version for migration support.
52    pub version: u32,
53}
54
55/// Context value types supporting common data types.
56///
57/// This provides a type-safe way to store heterogeneous values
58/// in the context while maintaining serializability.
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(tag = "type", content = "value")]
61pub enum ContextValue {
62    /// String value.
63    String(String),
64
65    /// Integer value (i64).
66    Integer(i64),
67
68    /// Floating point value (f64).
69    Float(f64),
70
71    /// Boolean value.
72    Boolean(bool),
73
74    /// List of values.
75    List(Vec<Self>),
76
77    /// Nested map of values.
78    Map(HashMap<String, Self>),
79
80    /// Null/None value.
81    Null,
82}
83
84impl Context {
85    /// Creates a new empty context with current timestamp.
86    #[must_use]
87    pub fn new() -> Self {
88        let now = current_timestamp();
89        Self {
90            variables: HashMap::new(),
91            globals: HashMap::new(),
92            buffer_ids: Vec::new(),
93            cwd: None,
94            metadata: ContextMetadata {
95                created_at: now,
96                updated_at: now,
97                version: 1,
98            },
99        }
100    }
101
102    /// Sets a context variable.
103    ///
104    /// # Arguments
105    ///
106    /// * `key` - Variable name.
107    /// * `value` - Variable value.
108    pub fn set_variable(&mut self, key: String, value: ContextValue) {
109        self.variables.insert(key, value);
110        self.touch();
111    }
112
113    /// Gets a context variable by key.
114    ///
115    /// # Arguments
116    ///
117    /// * `key` - Variable name to look up.
118    ///
119    /// # Returns
120    ///
121    /// Reference to the value if found.
122    #[must_use]
123    pub fn get_variable(&self, key: &str) -> Option<&ContextValue> {
124        self.variables.get(key)
125    }
126
127    /// Removes a context variable.
128    ///
129    /// # Arguments
130    ///
131    /// * `key` - Variable name to remove.
132    ///
133    /// # Returns
134    ///
135    /// The removed value if it existed.
136    pub fn remove_variable(&mut self, key: &str) -> Option<ContextValue> {
137        let result = self.variables.remove(key);
138        if result.is_some() {
139            self.touch();
140        }
141        result
142    }
143
144    /// Sets a global variable (persisted across sessions).
145    ///
146    /// # Arguments
147    ///
148    /// * `key` - Global variable name.
149    /// * `value` - Global variable value.
150    pub fn set_global(&mut self, key: String, value: ContextValue) {
151        self.globals.insert(key, value);
152        self.touch();
153    }
154
155    /// Gets a global variable by key.
156    ///
157    /// # Arguments
158    ///
159    /// * `key` - Global variable name to look up.
160    ///
161    /// # Returns
162    ///
163    /// Reference to the value if found.
164    #[must_use]
165    pub fn get_global(&self, key: &str) -> Option<&ContextValue> {
166        self.globals.get(key)
167    }
168
169    /// Removes a global variable.
170    ///
171    /// # Arguments
172    ///
173    /// * `key` - Global variable name to remove.
174    ///
175    /// # Returns
176    ///
177    /// The removed value if it existed.
178    pub fn remove_global(&mut self, key: &str) -> Option<ContextValue> {
179        let result = self.globals.remove(key);
180        if result.is_some() {
181            self.touch();
182        }
183        result
184    }
185
186    /// Adds a buffer ID to the active buffers list.
187    ///
188    /// # Arguments
189    ///
190    /// * `buffer_id` - ID of the buffer to add.
191    pub fn add_buffer(&mut self, buffer_id: i64) {
192        if !self.buffer_ids.contains(&buffer_id) {
193            self.buffer_ids.push(buffer_id);
194            self.touch();
195        }
196    }
197
198    /// Removes a buffer ID from the active buffers list.
199    ///
200    /// # Arguments
201    ///
202    /// * `buffer_id` - ID of the buffer to remove.
203    ///
204    /// # Returns
205    ///
206    /// `true` if the buffer was removed, `false` if not found.
207    pub fn remove_buffer(&mut self, buffer_id: i64) -> bool {
208        if let Some(pos) = self.buffer_ids.iter().position(|&id| id == buffer_id) {
209            self.buffer_ids.remove(pos);
210            self.touch();
211            true
212        } else {
213            false
214        }
215    }
216
217    /// Resets the context to empty state, preserving metadata.
218    pub fn reset(&mut self) {
219        self.variables.clear();
220        self.globals.clear();
221        self.buffer_ids.clear();
222        self.cwd = None;
223        self.touch();
224    }
225
226    /// Returns the number of variables in the context.
227    #[must_use]
228    pub fn variable_count(&self) -> usize {
229        self.variables.len()
230    }
231
232    /// Returns the number of globals in the context.
233    #[must_use]
234    pub fn global_count(&self) -> usize {
235        self.globals.len()
236    }
237
238    /// Returns the number of active buffers.
239    #[must_use]
240    pub const fn buffer_count(&self) -> usize {
241        self.buffer_ids.len()
242    }
243
244    /// Updates the `updated_at` timestamp.
245    fn touch(&mut self) {
246        self.metadata.updated_at = current_timestamp();
247    }
248}
249
250impl From<String> for ContextValue {
251    fn from(s: String) -> Self {
252        Self::String(s)
253    }
254}
255
256impl From<&str> for ContextValue {
257    fn from(s: &str) -> Self {
258        Self::String(s.to_string())
259    }
260}
261
262impl From<i64> for ContextValue {
263    fn from(n: i64) -> Self {
264        Self::Integer(n)
265    }
266}
267
268impl From<i32> for ContextValue {
269    fn from(n: i32) -> Self {
270        Self::Integer(i64::from(n))
271    }
272}
273
274impl From<f64> for ContextValue {
275    fn from(n: f64) -> Self {
276        Self::Float(n)
277    }
278}
279
280impl From<bool> for ContextValue {
281    fn from(b: bool) -> Self {
282        Self::Boolean(b)
283    }
284}
285
286#[allow(clippy::use_self)]
287impl<T: Into<ContextValue>> From<Vec<T>> for ContextValue {
288    fn from(v: Vec<T>) -> Self {
289        Self::List(v.into_iter().map(Into::into).collect())
290    }
291}
292
293#[allow(clippy::use_self)]
294impl<T: Into<ContextValue>> From<Option<T>> for ContextValue {
295    fn from(opt: Option<T>) -> Self {
296        opt.map_or(Self::Null, Into::into)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_context_new() {
306        let ctx = Context::new();
307        assert!(ctx.variables.is_empty());
308        assert!(ctx.globals.is_empty());
309        assert!(ctx.buffer_ids.is_empty());
310        assert!(ctx.cwd.is_none());
311        assert!(ctx.metadata.created_at > 0);
312    }
313
314    #[test]
315    fn test_variable_operations() {
316        let mut ctx = Context::new();
317
318        ctx.set_variable("key1".to_string(), "value1".into());
319        ctx.set_variable("key2".to_string(), 42i64.into());
320
321        assert_eq!(
322            ctx.get_variable("key1"),
323            Some(&ContextValue::String("value1".to_string()))
324        );
325        assert_eq!(ctx.get_variable("key2"), Some(&ContextValue::Integer(42)));
326        assert_eq!(ctx.get_variable("nonexistent"), None);
327        assert_eq!(ctx.variable_count(), 2);
328
329        let removed = ctx.remove_variable("key1");
330        assert!(removed.is_some());
331        assert_eq!(ctx.variable_count(), 1);
332    }
333
334    #[test]
335    fn test_global_operations() {
336        let mut ctx = Context::new();
337
338        ctx.set_global("global1".to_string(), true.into());
339        assert_eq!(
340            ctx.get_global("global1"),
341            Some(&ContextValue::Boolean(true))
342        );
343        assert_eq!(ctx.global_count(), 1);
344
345        ctx.remove_global("global1");
346        assert_eq!(ctx.global_count(), 0);
347    }
348
349    #[test]
350    fn test_buffer_operations() {
351        let mut ctx = Context::new();
352
353        ctx.add_buffer(1);
354        ctx.add_buffer(2);
355        ctx.add_buffer(1); // Duplicate, should not add
356
357        assert_eq!(ctx.buffer_count(), 2);
358        assert!(ctx.buffer_ids.contains(&1));
359        assert!(ctx.buffer_ids.contains(&2));
360
361        assert!(ctx.remove_buffer(1));
362        assert!(!ctx.remove_buffer(99)); // Non-existent
363        assert_eq!(ctx.buffer_count(), 1);
364    }
365
366    #[test]
367    fn test_context_reset() {
368        let mut ctx = Context::new();
369        ctx.set_variable("key".to_string(), "value".into());
370        ctx.set_global("global".to_string(), 1i64.into());
371        ctx.add_buffer(1);
372        ctx.cwd = Some("/tmp".to_string());
373
374        ctx.reset();
375
376        assert!(ctx.variables.is_empty());
377        assert!(ctx.globals.is_empty());
378        assert!(ctx.buffer_ids.is_empty());
379        assert!(ctx.cwd.is_none());
380    }
381
382    #[test]
383    fn test_context_value_conversions() {
384        let s: ContextValue = "test".into();
385        assert!(matches!(s, ContextValue::String(_)));
386
387        let i: ContextValue = 42i64.into();
388        assert!(matches!(i, ContextValue::Integer(42)));
389
390        let f: ContextValue = std::f64::consts::PI.into();
391        assert!(matches!(f, ContextValue::Float(_)));
392
393        let b: ContextValue = true.into();
394        assert!(matches!(b, ContextValue::Boolean(true)));
395
396        let none: ContextValue = Option::<String>::None.into();
397        assert!(matches!(none, ContextValue::Null));
398    }
399
400    #[test]
401    fn test_context_serialization() {
402        let mut ctx = Context::new();
403        ctx.set_variable("key".to_string(), "value".into());
404
405        let json = serde_json::to_string(&ctx);
406        assert!(json.is_ok());
407
408        let deserialized: Result<Context, _> = serde_json::from_str(&json.unwrap());
409        assert!(deserialized.is_ok());
410        assert_eq!(
411            deserialized.unwrap().get_variable("key"),
412            ctx.get_variable("key")
413        );
414    }
415
416    #[test]
417    fn test_touch_updates_timestamp() {
418        let mut ctx = Context::new();
419        let initial = ctx.metadata.updated_at;
420
421        // Small delay to ensure timestamp changes
422        std::thread::sleep(std::time::Duration::from_millis(10));
423
424        ctx.set_variable("key".to_string(), "value".into());
425        assert!(ctx.metadata.updated_at >= initial);
426    }
427
428    #[test]
429    fn test_context_value_from_string_owned() {
430        let s = String::from("owned string");
431        let cv: ContextValue = s.into();
432        assert!(matches!(cv, ContextValue::String(ref v) if v == "owned string"));
433    }
434
435    #[test]
436    fn test_context_value_from_i32() {
437        let n: i32 = 42;
438        let cv: ContextValue = n.into();
439        assert!(matches!(cv, ContextValue::Integer(42)));
440    }
441
442    #[test]
443    fn test_context_value_from_vec() {
444        let v: Vec<i64> = vec![1, 2, 3];
445        let cv: ContextValue = v.into();
446        if let ContextValue::List(list) = cv {
447            assert_eq!(list.len(), 3);
448            assert!(matches!(list[0], ContextValue::Integer(1)));
449            assert!(matches!(list[1], ContextValue::Integer(2)));
450            assert!(matches!(list[2], ContextValue::Integer(3)));
451        } else {
452            unreachable!("Expected List variant");
453        }
454    }
455
456    #[test]
457    fn test_context_value_from_option_some() {
458        let opt: Option<i64> = Some(42);
459        let cv: ContextValue = opt.into();
460        assert!(matches!(cv, ContextValue::Integer(42)));
461    }
462}