1use crate::io::current_timestamp;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
25pub struct Context {
26 pub variables: HashMap<String, ContextValue>,
28
29 pub globals: HashMap<String, ContextValue>,
31
32 pub buffer_ids: Vec<i64>,
34
35 pub cwd: Option<String>,
37
38 pub metadata: ContextMetadata,
40}
41
42#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub struct ContextMetadata {
45 pub created_at: i64,
47
48 pub updated_at: i64,
50
51 pub version: u32,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(tag = "type", content = "value")]
61pub enum ContextValue {
62 String(String),
64
65 Integer(i64),
67
68 Float(f64),
70
71 Boolean(bool),
73
74 List(Vec<Self>),
76
77 Map(HashMap<String, Self>),
79
80 Null,
82}
83
84impl Context {
85 #[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 pub fn set_variable(&mut self, key: String, value: ContextValue) {
109 self.variables.insert(key, value);
110 self.touch();
111 }
112
113 #[must_use]
123 pub fn get_variable(&self, key: &str) -> Option<&ContextValue> {
124 self.variables.get(key)
125 }
126
127 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 pub fn set_global(&mut self, key: String, value: ContextValue) {
151 self.globals.insert(key, value);
152 self.touch();
153 }
154
155 #[must_use]
165 pub fn get_global(&self, key: &str) -> Option<&ContextValue> {
166 self.globals.get(key)
167 }
168
169 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 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 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 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 #[must_use]
228 pub fn variable_count(&self) -> usize {
229 self.variables.len()
230 }
231
232 #[must_use]
234 pub fn global_count(&self) -> usize {
235 self.globals.len()
236 }
237
238 #[must_use]
240 pub const fn buffer_count(&self) -> usize {
241 self.buffer_ids.len()
242 }
243
244 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); 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)); 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 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}