cloacina_workflow/
context.rs

1/*
2 *  Copyright 2025 Colliery Software
3 *
4 *  Licensed under the Apache License, Version 2.0 (the "License");
5 *  you may not use this file except in compliance with the License.
6 *  You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 *  Unless required by applicable law or agreed to in writing, software
11 *  distributed under the License is distributed on an "AS IS" BASIS,
12 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *  See the License for the specific language governing permissions and
14 *  limitations under the License.
15 */
16
17//! # Minimal Context for Workflow Authoring
18//!
19//! This module provides a minimal `Context` type for sharing data between tasks.
20//! It contains only the core data operations without runtime-specific features
21//! like database persistence or dependency loading.
22
23use crate::error::ContextError;
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26use std::fmt::Debug;
27use tracing::{debug, warn};
28
29/// A context that holds data for pipeline execution.
30///
31/// The context is a type-safe, serializable container that flows through your pipeline,
32/// allowing tasks to share data. It supports JSON serialization and provides key-value
33/// access patterns with comprehensive error handling.
34///
35/// ## Type Parameter
36///
37/// - `T`: The type of values stored in the context. Must implement `Serialize`, `Deserialize`, and `Debug`.
38///
39/// ## Examples
40///
41/// ```rust
42/// use cloacina_workflow::Context;
43/// use serde_json::Value;
44///
45/// // Create a context for JSON values
46/// let mut context = Context::<Value>::new();
47///
48/// // Insert and retrieve data
49/// context.insert("user_id", serde_json::json!(123)).unwrap();
50/// let user_id = context.get("user_id").unwrap();
51/// ```
52#[derive(Debug)]
53pub struct Context<T>
54where
55    T: Serialize + for<'de> Deserialize<'de> + Debug,
56{
57    data: HashMap<String, T>,
58}
59
60impl<T> Context<T>
61where
62    T: Serialize + for<'de> Deserialize<'de> + Debug,
63{
64    /// Creates a new empty context.
65    ///
66    /// # Examples
67    ///
68    /// ```rust
69    /// use cloacina_workflow::Context;
70    ///
71    /// let context = Context::<i32>::new();
72    /// assert!(context.get("any_key").is_none());
73    /// ```
74    pub fn new() -> Self {
75        debug!("Creating new empty context");
76        Self {
77            data: HashMap::new(),
78        }
79    }
80
81    /// Creates a clone of this context's data.
82    ///
83    /// # Performance
84    ///
85    /// - Time complexity: O(n) where n is the number of key-value pairs
86    /// - Space complexity: O(n) for the cloned data
87    pub fn clone_data(&self) -> Self
88    where
89        T: Clone,
90    {
91        debug!("Cloning context data");
92        Self {
93            data: self.data.clone(),
94        }
95    }
96
97    /// Inserts a value into the context.
98    ///
99    /// # Arguments
100    ///
101    /// * `key` - The key to insert (can be any type that converts to String)
102    /// * `value` - The value to store
103    ///
104    /// # Returns
105    ///
106    /// * `Ok(())` - If the insertion was successful
107    /// * `Err(ContextError::KeyExists)` - If the key already exists
108    ///
109    /// # Examples
110    ///
111    /// ```rust
112    /// use cloacina_workflow::{Context, ContextError};
113    ///
114    /// let mut context = Context::<i32>::new();
115    ///
116    /// // First insertion succeeds
117    /// assert!(context.insert("count", 42).is_ok());
118    ///
119    /// // Duplicate insertion fails
120    /// assert!(matches!(context.insert("count", 43), Err(ContextError::KeyExists(_))));
121    /// ```
122    pub fn insert(&mut self, key: impl Into<String>, value: T) -> Result<(), ContextError> {
123        let key = key.into();
124        if self.data.contains_key(&key) {
125            warn!("Attempted to insert duplicate key: {}", key);
126            return Err(ContextError::KeyExists(key));
127        }
128        debug!("Inserting value for key: {}", key);
129        self.data.insert(key, value);
130        Ok(())
131    }
132
133    /// Updates an existing value in the context.
134    ///
135    /// # Arguments
136    ///
137    /// * `key` - The key to update
138    /// * `value` - The new value
139    ///
140    /// # Returns
141    ///
142    /// * `Ok(())` - If the update was successful
143    /// * `Err(ContextError::KeyNotFound)` - If the key doesn't exist
144    ///
145    /// # Examples
146    ///
147    /// ```rust
148    /// use cloacina_workflow::{Context, ContextError};
149    ///
150    /// let mut context = Context::<i32>::new();
151    /// context.insert("count", 42).unwrap();
152    ///
153    /// // Update existing key
154    /// assert!(context.update("count", 100).is_ok());
155    /// assert_eq!(context.get("count"), Some(&100));
156    ///
157    /// // Update non-existent key fails
158    /// assert!(matches!(context.update("missing", 1), Err(ContextError::KeyNotFound(_))));
159    /// ```
160    pub fn update(&mut self, key: impl Into<String>, value: T) -> Result<(), ContextError> {
161        let key = key.into();
162        if !self.data.contains_key(&key) {
163            warn!("Attempted to update non-existent key: {}", key);
164            return Err(ContextError::KeyNotFound(key));
165        }
166        debug!("Updating value for key: {}", key);
167        self.data.insert(key, value);
168        Ok(())
169    }
170
171    /// Gets a reference to a value from the context.
172    ///
173    /// # Arguments
174    ///
175    /// * `key` - The key to look up
176    ///
177    /// # Returns
178    ///
179    /// * `Some(&T)` - If the key exists
180    /// * `None` - If the key doesn't exist
181    ///
182    /// # Examples
183    ///
184    /// ```rust
185    /// use cloacina_workflow::Context;
186    ///
187    /// let mut context = Context::<String>::new();
188    /// context.insert("message", "Hello".to_string()).unwrap();
189    ///
190    /// assert_eq!(context.get("message"), Some(&"Hello".to_string()));
191    /// assert_eq!(context.get("missing"), None);
192    /// ```
193    pub fn get(&self, key: &str) -> Option<&T> {
194        debug!("Getting value for key: {}", key);
195        self.data.get(key)
196    }
197
198    /// Removes and returns a value from the context.
199    ///
200    /// # Arguments
201    ///
202    /// * `key` - The key to remove
203    ///
204    /// # Returns
205    ///
206    /// * `Some(T)` - If the key existed and was removed
207    /// * `None` - If the key didn't exist
208    ///
209    /// # Examples
210    ///
211    /// ```rust
212    /// use cloacina_workflow::Context;
213    ///
214    /// let mut context = Context::<i32>::new();
215    /// context.insert("temp", 42).unwrap();
216    ///
217    /// assert_eq!(context.remove("temp"), Some(42));
218    /// assert_eq!(context.get("temp"), None);
219    /// assert_eq!(context.remove("missing"), None);
220    /// ```
221    pub fn remove(&mut self, key: &str) -> Option<T> {
222        debug!("Removing value for key: {}", key);
223        self.data.remove(key)
224    }
225
226    /// Gets a reference to the underlying data HashMap.
227    ///
228    /// This method provides direct access to the internal data structure
229    /// for advanced use cases that need to iterate over all key-value pairs.
230    ///
231    /// # Returns
232    ///
233    /// A reference to the HashMap containing all context data
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use cloacina_workflow::Context;
239    ///
240    /// let mut context = Context::<i32>::new();
241    /// context.insert("a", 1).unwrap();
242    /// context.insert("b", 2).unwrap();
243    ///
244    /// for (key, value) in context.data() {
245    ///     println!("{}: {}", key, value);
246    /// }
247    /// ```
248    pub fn data(&self) -> &HashMap<String, T> {
249        &self.data
250    }
251
252    /// Consumes the context and returns the underlying data HashMap.
253    ///
254    /// # Returns
255    ///
256    /// The HashMap containing all context data
257    pub fn into_data(self) -> HashMap<String, T> {
258        self.data
259    }
260
261    /// Creates a Context from a HashMap.
262    ///
263    /// # Arguments
264    ///
265    /// * `data` - The HashMap to use as context data
266    ///
267    /// # Returns
268    ///
269    /// A new Context with the provided data
270    pub fn from_data(data: HashMap<String, T>) -> Self {
271        Self { data }
272    }
273
274    /// Serializes the context to a JSON string.
275    ///
276    /// # Returns
277    ///
278    /// * `Ok(String)` - The JSON representation of the context
279    /// * `Err(ContextError)` - If serialization fails
280    pub fn to_json(&self) -> Result<String, ContextError> {
281        debug!("Serializing context to JSON");
282        let json = serde_json::to_string(&self.data)?;
283        debug!("Context serialized successfully");
284        Ok(json)
285    }
286
287    /// Deserializes a context from a JSON string.
288    ///
289    /// # Arguments
290    ///
291    /// * `json` - The JSON string to deserialize
292    ///
293    /// # Returns
294    ///
295    /// * `Ok(Context<T>)` - The deserialized context
296    /// * `Err(ContextError)` - If deserialization fails
297    pub fn from_json(json: String) -> Result<Self, ContextError> {
298        debug!("Deserializing context from JSON");
299        let data = serde_json::from_str(&json)?;
300        debug!("Context deserialized successfully");
301        Ok(Self { data })
302    }
303}
304
305impl<T> Default for Context<T>
306where
307    T: Serialize + for<'de> Deserialize<'de> + Debug,
308{
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    fn setup_test_context() -> Context<i32> {
319        Context::new()
320    }
321
322    #[test]
323    fn test_context_operations() {
324        let mut context = setup_test_context();
325
326        // Test empty context
327        assert!(context.data.is_empty());
328
329        // Test insert and get
330        context.insert("test", 42).unwrap();
331        assert_eq!(context.get("test"), Some(&42));
332
333        // Test duplicate insert fails
334        assert!(matches!(
335            context.insert("test", 43),
336            Err(ContextError::KeyExists(_))
337        ));
338
339        // Test update
340        context.update("test", 43).unwrap();
341        assert_eq!(context.get("test"), Some(&43));
342
343        // Test update nonexistent key fails
344        assert!(matches!(
345            context.update("nonexistent", 42),
346            Err(ContextError::KeyNotFound(_))
347        ));
348    }
349
350    #[test]
351    fn test_context_serialization() {
352        let mut context = setup_test_context();
353        context.insert("test", 42).unwrap();
354
355        let json = context.to_json().unwrap();
356        let deserialized = Context::<i32>::from_json(json).unwrap();
357
358        assert_eq!(deserialized.get("test"), Some(&42));
359    }
360
361    #[test]
362    fn test_context_clone_data() {
363        let mut context = Context::<i32>::new();
364        context.insert("a", 1).unwrap();
365        context.insert("b", 2).unwrap();
366
367        let cloned = context.clone_data();
368        assert_eq!(cloned.get("a"), Some(&1));
369        assert_eq!(cloned.get("b"), Some(&2));
370    }
371
372    #[test]
373    fn test_context_from_data() {
374        let mut data = HashMap::new();
375        data.insert("key".to_string(), 42);
376
377        let context = Context::from_data(data);
378        assert_eq!(context.get("key"), Some(&42));
379    }
380
381    #[test]
382    fn test_context_into_data() {
383        let mut context = Context::<i32>::new();
384        context.insert("key", 42).unwrap();
385
386        let data = context.into_data();
387        assert_eq!(data.get("key"), Some(&42));
388    }
389}