cloacina-workflow::context
Rust
Minimal Context for Workflow Authoring
This module provides a minimal Context type for sharing data between tasks.
It contains only the core data operations without runtime-specific features
like database persistence or dependency loading.
Structs
Section titled “Structs”struct Context<T = serde_json::Value>
Section titled “struct Context<T = serde_json::Value>”pub
A context that holds data for pipeline execution.
The context is a type-safe, serializable container that flows through your pipeline, allowing tasks to share data. It supports JSON serialization and provides key-value access patterns with comprehensive error handling.
Type Parameter
T: The type of values stored in the context. Must implementSerialize,Deserialize, andDebug.
Examples:
use cloacina_workflow::Context;use serde_json::Value;
// Create a context for JSON valueslet mut context = Context::<Value>::new();
// Insert and retrieve datacontext.insert("user_id", serde_json::json!(123)).unwrap();let user_id = context.get("user_id").unwrap();Methods
Section titled “Methods”pub
fn new() -> SelfCreates a new empty context.
Examples:
use cloacina_workflow::Context;
let context = Context::<i32>::new();assert!(context.get("any_key").is_none());Source
pub fn new() -> Self { debug!("Creating new empty context"); Self { data: HashMap::new(), secrets: None, } }clone_data
Section titled “clone_data”pub
fn clone_data(&self) -> Selfwhere T: Clone,Creates a clone of this context’s data.
Performance
- Time complexity: O(n) where n is the number of key-value pairs
- Space complexity: O(n) for the cloned data
Source
pub fn clone_data(&self) -> Self where T: Clone, { debug!("Cloning context data"); Self { data: self.data.clone(), // Carry the resolver handle (cheap Arc clone) so a cloned execution // scope can still resolve secrets. secrets: self.secrets.clone(), } }insert
Section titled “insert”pub
fn insert( &mut self) -> Result<(, key: impl Into<String>) -> Result<(, value: T) -> Result<(,) -> Result<(), ContextError>Inserts a value into the context.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
impl Into<String> |
The key to insert (can be any type that converts to String) |
value |
T |
The value to store |
Returns:
Ok(())- If the insertion was successfulErr(ContextError::KeyExists)- If the key already exists
Examples:
use cloacina_workflow::{Context, ContextError};
let mut context = Context::<i32>::new();
// First insertion succeedsassert!(context.insert("count", 42).is_ok());
// Duplicate insertion failsassert!(matches!(context.insert("count", 43), Err(ContextError::KeyExists(_))));Source
pub fn insert(&mut self, key: impl Into<String>, value: T) -> Result<(), ContextError> { let key = key.into(); if self.data.contains_key(&key) { warn!("Attempted to insert duplicate key: {}", key); return Err(ContextError::KeyExists(key)); } debug!("Inserting value for key: {}", key); self.data.insert(key, value); Ok(()) }update
Section titled “update”pub
fn update( &mut self) -> Result<(, key: impl Into<String>) -> Result<(, value: T) -> Result<(,) -> Result<(), ContextError>Updates an existing value in the context.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
impl Into<String> |
The key to update |
value |
T |
The new value |
Returns:
Ok(())- If the update was successfulErr(ContextError::KeyNotFound)- If the key doesn’t exist
Examples:
use cloacina_workflow::{Context, ContextError};
let mut context = Context::<i32>::new();context.insert("count", 42).unwrap();
// Update existing keyassert!(context.update("count", 100).is_ok());assert_eq!(context.get("count"), Some(&100));
// Update non-existent key failsassert!(matches!(context.update("missing", 1), Err(ContextError::KeyNotFound(_))));Source
pub fn update(&mut self, key: impl Into<String>, value: T) -> Result<(), ContextError> { let key = key.into(); if !self.data.contains_key(&key) { warn!("Attempted to update non-existent key: {}", key); return Err(ContextError::KeyNotFound(key)); } debug!("Updating value for key: {}", key); self.data.insert(key, value); Ok(()) }pub
fn get(&self, key: &str) -> Option<&T>Gets a reference to a value from the context.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
&str |
The key to look up |
Returns:
Some(&T)- If the key existsNone- If the key doesn’t exist
Examples:
use cloacina_workflow::Context;
let mut context = Context::<String>::new();context.insert("message", "Hello".to_string()).unwrap();
assert_eq!(context.get("message"), Some(&"Hello".to_string()));assert_eq!(context.get("missing"), None);Source
pub fn get(&self, key: &str) -> Option<&T> { debug!("Getting value for key: {}", key); self.data.get(key) }remove
Section titled “remove”pub
fn remove(&mut self, key: &str) -> Option<T>Removes and returns a value from the context.
Parameters:
| Name | Type | Description |
|---|---|---|
key |
&str |
The key to remove |
Returns:
Some(T)- If the key existed and was removedNone- If the key didn’t exist
Examples:
use cloacina_workflow::Context;
let mut context = Context::<i32>::new();context.insert("temp", 42).unwrap();
assert_eq!(context.remove("temp"), Some(42));assert_eq!(context.get("temp"), None);assert_eq!(context.remove("missing"), None);Source
pub fn remove(&mut self, key: &str) -> Option<T> { debug!("Removing value for key: {}", key); self.data.remove(key) }pub
fn data(&self) -> &HashMap<String, T>Gets a reference to the underlying data HashMap.
This method provides direct access to the internal data structure for advanced use cases that need to iterate over all key-value pairs.
Returns:
A reference to the HashMap containing all context data
Examples:
use cloacina_workflow::Context;
let mut context = Context::<i32>::new();context.insert("a", 1).unwrap();context.insert("b", 2).unwrap();
for (key, value) in context.data() { println!("{}: {}", key, value);}Source
pub fn data(&self) -> &HashMap<String, T> { &self.data }into_data
Section titled “into_data”pub
fn into_data(self) -> HashMap<String, T>Consumes the context and returns the underlying data HashMap.
Returns:
The HashMap containing all context data
Source
pub fn into_data(self) -> HashMap<String, T> { self.data }from_data
Section titled “from_data”pub
fn from_data(data: HashMap<String, T>) -> SelfCreates a Context from a HashMap.
Parameters:
| Name | Type | Description |
|---|---|---|
data |
HashMap<String, T> |
The HashMap to use as context data |
Returns:
A new Context with the provided data
Source
pub fn from_data(data: HashMap<String, T>) -> Self { Self { data, secrets: None, } }to_json
Section titled “to_json”pub
fn to_json(&self) -> Result<String, ContextError>Serializes the context to a JSON string.
Returns:
Ok(String)- The JSON representation of the contextErr(ContextError)- If serialization fails
Source
pub fn to_json(&self) -> Result<String, ContextError> { debug!("Serializing context to JSON"); let json = serde_json::to_string(&self.data)?; debug!("Context serialized successfully"); Ok(json) }from_json
Section titled “from_json”pub
fn from_json(json: String) -> Result<Self, ContextError>Deserializes a context from a JSON string.
Parameters:
| Name | Type | Description |
|---|---|---|
json |
String |
The JSON string to deserialize |
Returns:
Ok(Context<T>)- The deserialized contextErr(ContextError)- If deserialization fails
Source
pub fn from_json(json: String) -> Result<Self, ContextError> { debug!("Deserializing context from JSON"); let data = serde_json::from_str(&json)?; debug!("Context deserialized successfully"); // A deserialized context is a durable snapshot: it never carries a // resolver. The runtime re-attaches one at fire time if needed. Ok(Self { data, secrets: None, }) }with_secret_resolver
Section titled “with_secret_resolver”pub
fn with_secret_resolver( mut self, resolver: Arc<dyn SecretResolver>,) -> SelfAttach a secret resolver, builder-style.
The resolver is a runtime-only side channel: it is NEVER serialized (see
Context::to_json, which writes only data), which is what keeps a
resolved secret structurally out of the durable context.
Source
pub fn with_secret_resolver(mut self, resolver: Arc<dyn SecretResolver>) -> Self { self.secrets = Some(resolver); self }set_secret_resolver
Section titled “set_secret_resolver”pub
fn set_secret_resolver(&mut self, resolver: Arc<dyn SecretResolver>)Attach (or replace) the secret resolver on this scope.
Source
pub fn set_secret_resolver(&mut self, resolver: Arc<dyn SecretResolver>) { self.secrets = Some(resolver); }has_secret_resolver
Section titled “has_secret_resolver”pub
fn has_secret_resolver(&self) -> boolWhether a secret resolver is configured on this execution scope.
Source
pub fn has_secret_resolver(&self) -> bool { self.secrets.is_some() }secret
Section titled “secret”pub async
async fn secret( &self, name: &str,) -> Result<BTreeMap<String, String>, SecretAccessError>Resolve a named secret into its decrypted {field: value} map.
name may be either the concrete secret name OR a declared local
binding name that an instance mapped to a secret via a
{"$secret": "..."} reference (CLOACI-I-0133 / T-0859). When the
context carries a {"$secret"} alias map (under
secret::SECRET_REFS_KEY) and name
appears as a local binding there, the mapped secret name is resolved
instead — so a task can read either the declared name it authored against
or the concrete secret the instance chose.
The returned map is handed to the task and is NEVER inserted into the
context’s serialized data. Errors clearly when no resolver is
configured (SecretAccessError::NotConfigured) or the name is absent
(SecretAccessError::NotFound).
Source
pub async fn secret(&self, name: &str) -> Result<BTreeMap<String, String>, SecretAccessError> { let resolver = self .secrets .as_ref() .ok_or(SecretAccessError::NotConfigured)?; let effective = self.resolve_secret_alias(name); resolver.resolve(&effective).await.map_err(|e| match e { SecretResolverError::NotFound(n) => SecretAccessError::NotFound(n), SecretResolverError::NotGranted(n) => SecretAccessError::NotGranted(n), SecretResolverError::Backend(m) => SecretAccessError::Backend(m), }) }secret_field
Section titled “secret_field”pub async
async fn secret_field( &self, name: &str, field: &str,) -> Result<String, SecretAccessError>Resolve one field of a named secret.
Convenience over Context::secret; errors with
SecretAccessError::FieldNotFound when the secret exists but lacks the
requested field.
Source
pub async fn secret_field(&self, name: &str, field: &str) -> Result<String, SecretAccessError> { let fields = self.secret(name).await?; fields .get(field) .cloned() .ok_or_else(|| SecretAccessError::FieldNotFound { secret: name.to_string(), field: field.to_string(), }) }get_as
Section titled “get_as”pub
fn get_as<V>(&self, key: &str) -> Result<Option<V>, crate::error::TaskError>where V: serde::de::DeserializeOwned,Get a value by key and deserialize it into V.
Returns Ok(None) when the key is absent, Ok(Some(value)) when it is
present and deserializes cleanly, and Err(TaskError::ValidationFailed)
when the stored JSON does not match V (the message names the key and
target type).
Examples:
use cloacina_workflow::Context;
let mut ctx = Context::new();ctx.insert("count", serde_json::json!(7)).unwrap();let n: Option<i64> = ctx.get_as("count").unwrap();assert_eq!(n, Some(7));assert_eq!(ctx.get_as::<i64>("missing").unwrap(), None);Source
pub fn get_as<V>(&self, key: &str) -> Result<Option<V>, crate::error::TaskError> where V: serde::de::DeserializeOwned, { match self.data.get(key) { None => Ok(None), Some(value) => serde_json::from_value(value.clone()) .map(Some) .map_err(|e| crate::error::TaskError::ValidationFailed { message: format!( "context key '{}' could not be read as {}: {}", key, std::any::type_name::<V>(), e ), }), } }get_required
Section titled “get_required”pub
fn get_required<V>(&self, key: &str) -> Result<V, crate::error::TaskError>where V: serde::de::DeserializeOwned,Get a value by key, deserialize it into V, and error if the key is missing.
Err(TaskError::ValidationFailed) when the key is absent or the stored
JSON does not match V.
Examples:
use cloacina_workflow::Context;
let mut ctx = Context::new();ctx.insert("name", serde_json::json!("ada")).unwrap();let name: String = ctx.get_required("name").unwrap();assert_eq!(name, "ada");assert!(ctx.get_required::<String>("missing").is_err());Source
pub fn get_required<V>(&self, key: &str) -> Result<V, crate::error::TaskError> where V: serde::de::DeserializeOwned, { match self.get_as(key)? { Some(value) => Ok(value), None => Err(crate::error::TaskError::ValidationFailed { message: format!( "required context key '{}' is missing (expected {})", key, std::any::type_name::<V>() ), }), } }insert_as
Section titled “insert_as”pub
fn insert_as<V>( &mut self, key: impl Into<String>, value: V,) -> Result<(), crate::error::TaskError>where V: serde::Serialize,Serialize a value and write it under key, upserting (insert or overwrite).
Folds the serde_json::json!(...) / to_value wrapping — and the
“exists? update : insert” dance — that every context write otherwise
repeats. Upsert semantics mirror Python’s context.set(key, value)
(unlike the lower-level Context::insert, which errors on an existing
key). Errors with TaskError::ValidationFailed only if the value cannot
be serialized.
Examples:
use cloacina_workflow::Context;
let mut ctx = Context::new();ctx.insert_as("total", 42u32).unwrap();assert_eq!(ctx.get_as::<u32>("total").unwrap(), Some(42));// Upserts — overwriting an existing key is fine.ctx.insert_as("total", 100u32).unwrap();assert_eq!(ctx.get_as::<u32>("total").unwrap(), Some(100));Source
pub fn insert_as<V>( &mut self, key: impl Into<String>, value: V, ) -> Result<(), crate::error::TaskError> where V: serde::Serialize, { let key = key.into(); let json = serde_json::to_value(value).map_err(|e| crate::error::TaskError::ValidationFailed { message: format!("context key '{}' could not be serialized: {}", key, e), })?; // Upsert: overwrite if present, insert otherwise. self.data.insert(key, json); Ok(()) }