agentic_workflow/governance/
variable.rs1use 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
11pub 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 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 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 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 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 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 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 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 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 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 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}