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}