ricecoder_workflows/
parameter_substitution.rs1use crate::error::{WorkflowError, WorkflowResult};
4use crate::models::{Workflow, WorkflowStep};
5use crate::parameters::ParameterSubstitutor;
6use serde_json::Value;
7use std::collections::HashMap;
8
9pub struct StepConfigSubstitutor;
11
12impl StepConfigSubstitutor {
13 pub fn substitute_in_workflow(
19 workflow: &mut Workflow,
20 parameters: &HashMap<String, Value>,
21 ) -> WorkflowResult<()> {
22 let final_params = Self::build_final_parameters(workflow, parameters)?;
24
25 for step in &mut workflow.steps {
27 Self::substitute_in_step(step, &final_params)?;
28 }
29
30 Ok(())
31 }
32
33 pub fn substitute_in_step(
35 step: &mut WorkflowStep,
36 parameters: &HashMap<String, Value>,
37 ) -> WorkflowResult<()> {
38 let substituted = ParameterSubstitutor::substitute(&step.config.config, parameters)?;
40 step.config.config = substituted;
41
42 Ok(())
43 }
44
45 fn validate_all_parameters_provided(
47 workflow: &Workflow,
48 parameters: &HashMap<String, Value>,
49 ) -> WorkflowResult<()> {
50 for param_def in &workflow.parameters {
52 if param_def.required
53 && !parameters.contains_key(¶m_def.name)
54 && param_def.default.is_none()
55 {
56 return Err(WorkflowError::Invalid(format!(
57 "Required parameter '{}' not provided",
58 param_def.name
59 )));
60 }
61 }
62
63 let known_names: std::collections::HashSet<_> =
65 workflow.parameters.iter().map(|p| &p.name).collect();
66
67 for provided_name in parameters.keys() {
68 if !known_names.contains(provided_name) {
69 return Err(WorkflowError::Invalid(format!(
70 "Unknown parameter: {}",
71 provided_name
72 )));
73 }
74 }
75
76 Ok(())
77 }
78
79 pub fn build_final_parameters(
83 workflow: &Workflow,
84 provided: &HashMap<String, Value>,
85 ) -> WorkflowResult<HashMap<String, Value>> {
86 Self::validate_all_parameters_provided(workflow, provided)?;
87
88 let mut final_params = HashMap::new();
89
90 for param_def in &workflow.parameters {
91 if let Some(value) = provided.get(¶m_def.name) {
92 final_params.insert(param_def.name.clone(), value.clone());
93 } else if let Some(default) = ¶m_def.default {
94 final_params.insert(param_def.name.clone(), default.clone());
95 }
96 }
97
98 Ok(final_params)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::models::StepConfig;
106 use serde_json::json;
107
108 fn create_test_workflow() -> Workflow {
109 Workflow {
110 id: "test".to_string(),
111 name: "Test".to_string(),
112 description: "Test workflow".to_string(),
113 parameters: vec![
114 crate::models::WorkflowParameter {
115 name: "name".to_string(),
116 param_type: "string".to_string(),
117 default: Some(json!("default-name")),
118 required: false,
119 description: "Name parameter".to_string(),
120 },
121 crate::models::WorkflowParameter {
122 name: "count".to_string(),
123 param_type: "number".to_string(),
124 default: None,
125 required: true,
126 description: "Count parameter".to_string(),
127 },
128 ],
129 steps: vec![crate::models::WorkflowStep {
130 id: "step1".to_string(),
131 name: "Step 1".to_string(),
132 step_type: crate::models::StepType::Agent(crate::models::AgentStep {
133 agent_id: "test-agent".to_string(),
134 task: "test-task".to_string(),
135 }),
136 config: StepConfig {
137 config: json!({
138 "message": "Hello ${name}",
139 "count": "${count}"
140 }),
141 },
142 dependencies: vec![],
143 approval_required: false,
144 on_error: crate::models::ErrorAction::Fail,
145 risk_score: None,
146 risk_factors: crate::models::RiskFactors::default(),
147 }],
148 config: crate::models::WorkflowConfig {
149 timeout_ms: None,
150 max_parallel: None,
151 },
152 }
153 }
154
155 #[test]
156 fn test_substitute_in_step_config() {
157 let mut workflow = create_test_workflow();
158 let mut params = HashMap::new();
159 params.insert("name".to_string(), json!("Alice"));
160 params.insert("count".to_string(), json!(42));
161
162 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
163 assert!(result.is_ok());
164
165 let step = &workflow.steps[0];
166 assert_eq!(
167 step.config.config.get("message").and_then(|v| v.as_str()),
168 Some("Hello Alice")
169 );
170 }
171
172 #[test]
173 fn test_substitute_missing_required_parameter() {
174 let mut workflow = create_test_workflow();
175 let params = HashMap::new();
176
177 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
178 assert!(result.is_err());
179 }
180
181 #[test]
182 fn test_substitute_unknown_parameter() {
183 let mut workflow = create_test_workflow();
184 let mut params = HashMap::new();
185 params.insert("name".to_string(), json!("Alice"));
186 params.insert("count".to_string(), json!(42));
187 params.insert("unknown".to_string(), json!("value"));
188
189 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
190 assert!(result.is_err());
191 }
192
193 #[test]
194 fn test_build_final_parameters_with_defaults() {
195 let workflow = create_test_workflow();
196 let mut provided = HashMap::new();
197 provided.insert("count".to_string(), json!(42));
198
199 let result = StepConfigSubstitutor::build_final_parameters(&workflow, &provided);
200 assert!(result.is_ok());
201
202 let final_params = result.unwrap();
203 assert_eq!(final_params.get("name"), Some(&json!("default-name")));
204 assert_eq!(final_params.get("count"), Some(&json!(42)));
205 }
206
207 #[test]
208 fn test_substitute_nested_parameters() {
209 let mut workflow = create_test_workflow();
210 workflow.steps[0].config.config = json!({
211 "nested": {
212 "message": "Hello ${name}",
213 "items": ["${name}", "world"]
214 }
215 });
216
217 let mut params = HashMap::new();
218 params.insert("name".to_string(), json!("Alice"));
219 params.insert("count".to_string(), json!(42));
220
221 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
222 assert!(result.is_ok());
223
224 let step = &workflow.steps[0];
225 assert_eq!(
226 step.config
227 .config
228 .get("nested")
229 .and_then(|v| v.get("message"))
230 .and_then(|v| v.as_str()),
231 Some("Hello Alice")
232 );
233 }
234
235 #[test]
236 fn test_substitute_multiple_references_in_string() {
237 let mut workflow = create_test_workflow();
238 workflow.steps[0].config.config = json!({
239 "message": "${name} has ${count} items"
240 });
241
242 let mut params = HashMap::new();
243 params.insert("name".to_string(), json!("Alice"));
244 params.insert("count".to_string(), json!(42));
245
246 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
247 assert!(result.is_ok());
248
249 let step = &workflow.steps[0];
250 assert_eq!(
251 step.config.config.get("message").and_then(|v| v.as_str()),
252 Some("Alice has 42 items")
253 );
254 }
255
256 #[test]
257 fn test_substitute_with_default_parameter() {
258 let mut workflow = create_test_workflow();
259 let mut params = HashMap::new();
260 params.insert("count".to_string(), json!(42));
261 let result = StepConfigSubstitutor::substitute_in_workflow(&mut workflow, ¶ms);
264 assert!(result.is_ok());
265
266 let step = &workflow.steps[0];
267 assert_eq!(
268 step.config.config.get("message").and_then(|v| v.as_str()),
269 Some("Hello default-name")
270 );
271 }
272}