Skip to main content

agentic_workflow/governance/
variable.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use uuid::Uuid;
5
6use crate::types::{
7    ScopeType, ScopedVariable, TypeCheckError, TypeCheckResult, VariableScope,
8    VariableType, WorkflowError, WorkflowResult,
9};
10
11/// Hierarchical variable scoping engine.
12pub struct VariableEngine {
13    scopes: HashMap<String, VariableScope>,
14}
15
16impl VariableEngine {
17    pub fn new() -> Self {
18        Self {
19            scopes: HashMap::new(),
20        }
21    }
22
23    /// Create a new scope.
24    pub fn create_scope(
25        &mut self,
26        scope_type: ScopeType,
27        parent_scope_id: Option<&str>,
28    ) -> String {
29        let id = Uuid::new_v4().to_string();
30        let scope = VariableScope {
31            scope_id: id.clone(),
32            scope_type,
33            parent_scope_id: parent_scope_id.map(|s| s.to_string()),
34            variables: HashMap::new(),
35        };
36
37        self.scopes.insert(id.clone(), scope);
38        id
39    }
40
41    /// Set a variable in a scope.
42    pub fn set(
43        &mut self,
44        scope_id: &str,
45        name: &str,
46        value: serde_json::Value,
47        var_type: VariableType,
48        set_by: &str,
49    ) -> WorkflowResult<()> {
50        let scope = self
51            .scopes
52            .get_mut(scope_id)
53            .ok_or_else(|| WorkflowError::VariableNotFound(format!("Scope: {}", scope_id)))?;
54
55        // Check immutability
56        if let Some(existing) = scope.variables.get(name) {
57            if existing.immutable {
58                return Err(WorkflowError::Internal(format!(
59                    "Variable '{}' is immutable",
60                    name
61                )));
62            }
63        }
64
65        // Type check
66        if !var_type.matches(&value) {
67            return Err(WorkflowError::VariableTypeMismatch {
68                expected: format!("{:?}", var_type),
69                actual: format!("{}", value),
70            });
71        }
72
73        scope.variables.insert(
74            name.to_string(),
75            ScopedVariable {
76                name: name.to_string(),
77                value,
78                var_type,
79                immutable: false,
80                set_at: Utc::now(),
81                set_by: set_by.to_string(),
82            },
83        );
84
85        Ok(())
86    }
87
88    /// Get a variable, respecting scope hierarchy (child → parent cascade).
89    pub fn get(&self, scope_id: &str, name: &str) -> WorkflowResult<&ScopedVariable> {
90        let mut current_scope_id = Some(scope_id.to_string());
91
92        while let Some(sid) = current_scope_id {
93            if let Some(scope) = self.scopes.get(&sid) {
94                if let Some(var) = scope.variables.get(name) {
95                    return Ok(var);
96                }
97                current_scope_id = scope.parent_scope_id.clone();
98            } else {
99                break;
100            }
101        }
102
103        Err(WorkflowError::VariableNotFound(name.to_string()))
104    }
105
106    /// List all variables in a scope (not including parent).
107    pub fn list(&self, scope_id: &str) -> WorkflowResult<Vec<&ScopedVariable>> {
108        let scope = self
109            .scopes
110            .get(scope_id)
111            .ok_or_else(|| WorkflowError::VariableNotFound(format!("Scope: {}", scope_id)))?;
112
113        Ok(scope.variables.values().collect())
114    }
115
116    /// Promote a variable from child scope to parent scope.
117    pub fn promote(&mut self, scope_id: &str, name: &str) -> WorkflowResult<()> {
118        let (parent_id, var) = {
119            let scope = self
120                .scopes
121                .get(scope_id)
122                .ok_or_else(|| WorkflowError::VariableNotFound(format!("Scope: {}", scope_id)))?;
123
124            let var = scope
125                .variables
126                .get(name)
127                .ok_or_else(|| WorkflowError::VariableNotFound(name.to_string()))?
128                .clone();
129
130            let parent_id = scope
131                .parent_scope_id
132                .clone()
133                .ok_or_else(|| WorkflowError::Internal("No parent scope".to_string()))?;
134
135            (parent_id, var)
136        };
137
138        let parent = self
139            .scopes
140            .get_mut(&parent_id)
141            .ok_or_else(|| WorkflowError::Internal("Parent scope not found".to_string()))?;
142
143        parent.variables.insert(name.to_string(), var);
144        Ok(())
145    }
146
147    /// Type check all variables across scopes.
148    pub fn type_check(&self) -> TypeCheckResult {
149        let mut errors = Vec::new();
150
151        for scope in self.scopes.values() {
152            for var in scope.variables.values() {
153                if !var.var_type.matches(&var.value) {
154                    errors.push(TypeCheckError {
155                        variable_name: var.name.clone(),
156                        scope_id: scope.scope_id.clone(),
157                        expected: var.var_type.clone(),
158                        actual: format!("{}", var.value),
159                        message: format!(
160                            "Variable '{}' expected {:?} but got {}",
161                            var.name, var.var_type, var.value
162                        ),
163                    });
164                }
165            }
166        }
167
168        TypeCheckResult {
169            valid: errors.is_empty(),
170            errors,
171        }
172    }
173
174    /// Make a variable immutable.
175    pub fn make_immutable(&mut self, scope_id: &str, name: &str) -> WorkflowResult<()> {
176        let scope = self
177            .scopes
178            .get_mut(scope_id)
179            .ok_or_else(|| WorkflowError::VariableNotFound(format!("Scope: {}", scope_id)))?;
180
181        let var = scope
182            .variables
183            .get_mut(name)
184            .ok_or_else(|| WorkflowError::VariableNotFound(name.to_string()))?;
185
186        var.immutable = true;
187        Ok(())
188    }
189}
190
191impl Default for VariableEngine {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_variable_scope_hierarchy() {
203        let mut engine = VariableEngine::new();
204        let parent_id = engine.create_scope(ScopeType::Workflow, None);
205        let child_id = engine.create_scope(ScopeType::Step, Some(&parent_id));
206
207        engine
208            .set(&parent_id, "config", serde_json::json!("prod"), VariableType::String, "system")
209            .unwrap();
210
211        // Child can read parent's variable
212        let var = engine.get(&child_id, "config").unwrap();
213        assert_eq!(var.value, serde_json::json!("prod"));
214    }
215
216    #[test]
217    fn test_immutability() {
218        let mut engine = VariableEngine::new();
219        let sid = engine.create_scope(ScopeType::Workflow, None);
220
221        engine
222            .set(&sid, "frozen", serde_json::json!(42), VariableType::Integer, "system")
223            .unwrap();
224        engine.make_immutable(&sid, "frozen").unwrap();
225
226        let result = engine.set(&sid, "frozen", serde_json::json!(99), VariableType::Integer, "system");
227        assert!(result.is_err());
228    }
229}