1use crate::error::{WorkflowError, WorkflowResult};
4use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct ParameterDef {
10 pub name: String,
12 pub param_type: ParameterType,
14 pub default: Option<Value>,
16 pub required: bool,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ParameterType {
23 String,
25 Number,
27 Boolean,
29 Object,
31 Array,
33}
34
35impl ParameterType {
36 pub fn matches(&self, value: &Value) -> bool {
38 match self {
39 ParameterType::String => value.is_string(),
40 ParameterType::Number => value.is_number(),
41 ParameterType::Boolean => value.is_boolean(),
42 ParameterType::Object => value.is_object(),
43 ParameterType::Array => value.is_array(),
44 }
45 }
46}
47
48pub struct ParameterValidator;
50
51impl ParameterValidator {
52 pub fn validate_definitions(params: &[ParameterDef]) -> WorkflowResult<()> {
59 let mut seen_names = std::collections::HashSet::new();
60
61 for param in params {
62 if !seen_names.insert(¶m.name) {
64 return Err(WorkflowError::Invalid(format!(
65 "Duplicate parameter name: {}",
66 param.name
67 )));
68 }
69
70 if param.name.is_empty() {
72 return Err(WorkflowError::Invalid(
73 "Parameter name cannot be empty".to_string(),
74 ));
75 }
76
77 if !param
79 .name
80 .chars()
81 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
82 {
83 return Err(WorkflowError::Invalid(format!(
84 "Invalid parameter name: {}. Must contain only alphanumeric characters, underscores, and hyphens",
85 param.name
86 )));
87 }
88
89 if let Some(default) = ¶m.default {
91 if !param.param_type.matches(default) {
92 return Err(WorkflowError::Invalid(format!(
93 "Default value for parameter '{}' does not match type {:?}",
94 param.name, param.param_type
95 )));
96 }
97 }
98
99 if param.required && param.default.is_some() {
101 return Err(WorkflowError::Invalid(format!(
102 "Required parameter '{}' cannot have a default value",
103 param.name
104 )));
105 }
106 }
107
108 Ok(())
109 }
110
111 pub fn validate_values(
118 definitions: &[ParameterDef],
119 values: &HashMap<String, Value>,
120 ) -> WorkflowResult<()> {
121 let known_names: std::collections::HashSet<_> =
123 definitions.iter().map(|p| &p.name).collect();
124
125 for provided_name in values.keys() {
126 if !known_names.contains(provided_name) {
127 return Err(WorkflowError::Invalid(format!(
128 "Unknown parameter: {}",
129 provided_name
130 )));
131 }
132 }
133
134 for param_def in definitions {
136 match values.get(¶m_def.name) {
137 Some(value) => {
138 if !param_def.param_type.matches(value) {
140 let type_name = match value {
141 Value::String(_) => "string",
142 Value::Number(_) => "number",
143 Value::Bool(_) => "boolean",
144 Value::Array(_) => "array",
145 Value::Object(_) => "object",
146 Value::Null => "null",
147 };
148 return Err(WorkflowError::Invalid(format!(
149 "Parameter '{}' has incorrect type. Expected {:?}, got {}",
150 param_def.name, param_def.param_type, type_name
151 )));
152 }
153 }
154 None => {
155 if param_def.required && param_def.default.is_none() {
157 return Err(WorkflowError::Invalid(format!(
158 "Required parameter '{}' not provided",
159 param_def.name
160 )));
161 }
162 }
163 }
164 }
165
166 Ok(())
167 }
168
169 pub fn build_final_values(
173 definitions: &[ParameterDef],
174 provided: &HashMap<String, Value>,
175 ) -> WorkflowResult<HashMap<String, Value>> {
176 Self::validate_values(definitions, provided)?;
177
178 let mut final_values = HashMap::new();
179
180 for param_def in definitions {
181 if let Some(value) = provided.get(¶m_def.name) {
182 final_values.insert(param_def.name.clone(), value.clone());
183 } else if let Some(default) = ¶m_def.default {
184 final_values.insert(param_def.name.clone(), default.clone());
185 }
186 }
187
188 Ok(final_values)
189 }
190}
191
192pub struct ParameterSubstitutor;
194
195impl ParameterSubstitutor {
196 pub fn substitute(value: &Value, parameters: &HashMap<String, Value>) -> WorkflowResult<Value> {
201 match value {
202 Value::String(s) => Self::substitute_string(s, parameters),
203 Value::Object(obj) => {
204 let mut result = serde_json::Map::new();
205 for (key, val) in obj {
206 result.insert(key.clone(), Self::substitute(val, parameters)?);
207 }
208 Ok(Value::Object(result))
209 }
210 Value::Array(arr) => {
211 let result: WorkflowResult<Vec<_>> = arr
212 .iter()
213 .map(|v| Self::substitute(v, parameters))
214 .collect();
215 Ok(Value::Array(result?))
216 }
217 other => Ok(other.clone()),
218 }
219 }
220
221 fn substitute_string(s: &str, parameters: &HashMap<String, Value>) -> WorkflowResult<Value> {
226 let mut result = s.to_string();
227 let mut iterations = 0;
228 const MAX_ITERATIONS: usize = 10; loop {
231 iterations += 1;
232 if iterations > MAX_ITERATIONS {
233 return Err(WorkflowError::Invalid(
234 "Parameter substitution exceeded maximum iterations (possible circular reference)"
235 .to_string(),
236 ));
237 }
238
239 let mut found_any = false;
241 let mut new_result = result.clone();
242
243 let mut start = 0;
245 while let Some(pos) = new_result[start..].find("${") {
246 let actual_pos = start + pos;
247 if let Some(end_pos) = new_result[actual_pos + 2..].find('}') {
248 let actual_end = actual_pos + 2 + end_pos;
249 let param_name = &new_result[actual_pos + 2..actual_end];
250
251 if !param_name
253 .chars()
254 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
255 {
256 return Err(WorkflowError::Invalid(format!(
257 "Invalid parameter reference: ${{{}}}",
258 param_name
259 )));
260 }
261
262 match parameters.get(param_name) {
264 Some(value) => {
265 let replacement = match value {
266 Value::String(s) => s.clone(),
267 Value::Number(n) => n.to_string(),
268 Value::Bool(b) => b.to_string(),
269 Value::Null => "null".to_string(),
270 _ => {
271 return Err(WorkflowError::Invalid(format!(
272 "Cannot substitute complex type for parameter '{}'",
273 param_name
274 )))
275 }
276 };
277
278 new_result.replace_range(actual_pos..=actual_end, &replacement);
279 found_any = true;
280 start = actual_pos + replacement.len();
281 }
282 None => {
283 return Err(WorkflowError::Invalid(format!(
284 "Parameter '{}' not provided",
285 param_name
286 )));
287 }
288 }
289 } else {
290 start = actual_pos + 2;
291 }
292 }
293
294 result = new_result;
295
296 if !found_any {
297 break;
298 }
299 }
300
301 Ok(Value::String(result))
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use serde_json::json;
309
310 #[test]
311 fn test_validate_parameter_definitions_valid() {
312 let params = vec![
313 ParameterDef {
314 name: "name".to_string(),
315 param_type: ParameterType::String,
316 default: Some(json!("default-name")),
317 required: false,
318 },
319 ParameterDef {
320 name: "count".to_string(),
321 param_type: ParameterType::Number,
322 default: None,
323 required: true,
324 },
325 ];
326
327 assert!(ParameterValidator::validate_definitions(¶ms).is_ok());
328 }
329
330 #[test]
331 fn test_validate_parameter_definitions_duplicate_names() {
332 let params = vec![
333 ParameterDef {
334 name: "name".to_string(),
335 param_type: ParameterType::String,
336 default: None,
337 required: false,
338 },
339 ParameterDef {
340 name: "name".to_string(),
341 param_type: ParameterType::String,
342 default: None,
343 required: false,
344 },
345 ];
346
347 assert!(ParameterValidator::validate_definitions(¶ms).is_err());
348 }
349
350 #[test]
351 fn test_validate_parameter_definitions_type_mismatch() {
352 let params = vec![ParameterDef {
353 name: "count".to_string(),
354 param_type: ParameterType::Number,
355 default: Some(json!("not-a-number")),
356 required: false,
357 }];
358
359 assert!(ParameterValidator::validate_definitions(¶ms).is_err());
360 }
361
362 #[test]
363 fn test_validate_parameter_values_missing_required() {
364 let definitions = vec![ParameterDef {
365 name: "required_param".to_string(),
366 param_type: ParameterType::String,
367 default: None,
368 required: true,
369 }];
370
371 let values = HashMap::new();
372
373 assert!(ParameterValidator::validate_values(&definitions, &values).is_err());
374 }
375
376 #[test]
377 fn test_validate_parameter_values_unknown_parameter() {
378 let definitions = vec![ParameterDef {
379 name: "known".to_string(),
380 param_type: ParameterType::String,
381 default: None,
382 required: false,
383 }];
384
385 let mut values = HashMap::new();
386 values.insert("unknown".to_string(), json!("value"));
387
388 assert!(ParameterValidator::validate_values(&definitions, &values).is_err());
389 }
390
391 #[test]
392 fn test_validate_parameter_values_type_mismatch() {
393 let definitions = vec![ParameterDef {
394 name: "count".to_string(),
395 param_type: ParameterType::Number,
396 default: None,
397 required: true,
398 }];
399
400 let mut values = HashMap::new();
401 values.insert("count".to_string(), json!("not-a-number"));
402
403 assert!(ParameterValidator::validate_values(&definitions, &values).is_err());
404 }
405
406 #[test]
407 fn test_build_final_values_with_defaults() {
408 let definitions = vec![
409 ParameterDef {
410 name: "name".to_string(),
411 param_type: ParameterType::String,
412 default: Some(json!("default-name")),
413 required: false,
414 },
415 ParameterDef {
416 name: "count".to_string(),
417 param_type: ParameterType::Number,
418 default: None,
419 required: true,
420 },
421 ];
422
423 let mut provided = HashMap::new();
424 provided.insert("count".to_string(), json!(42));
425
426 let result = ParameterValidator::build_final_values(&definitions, &provided);
427 assert!(result.is_ok());
428
429 let final_values = result.unwrap();
430 assert_eq!(final_values.get("name"), Some(&json!("default-name")));
431 assert_eq!(final_values.get("count"), Some(&json!(42)));
432 }
433
434 #[test]
435 fn test_substitute_simple_string() {
436 let mut params = HashMap::new();
437 params.insert("name".to_string(), json!("Alice"));
438
439 let result = ParameterSubstitutor::substitute(&json!("Hello ${name}"), ¶ms);
440 assert!(result.is_ok());
441 assert_eq!(result.unwrap(), json!("Hello Alice"));
442 }
443
444 #[test]
445 fn test_substitute_multiple_references() {
446 let mut params = HashMap::new();
447 params.insert("first".to_string(), json!("Alice"));
448 params.insert("last".to_string(), json!("Smith"));
449
450 let result = ParameterSubstitutor::substitute(&json!("${first} ${last}"), ¶ms);
451 assert!(result.is_ok());
452 assert_eq!(result.unwrap(), json!("Alice Smith"));
453 }
454
455 #[test]
456 fn test_substitute_in_object() {
457 let mut params = HashMap::new();
458 params.insert("name".to_string(), json!("Alice"));
459
460 let input = json!({
461 "greeting": "Hello ${name}",
462 "nested": {
463 "message": "Welcome ${name}"
464 }
465 });
466
467 let result = ParameterSubstitutor::substitute(&input, ¶ms);
468 assert!(result.is_ok());
469
470 let result = result.unwrap();
471 assert_eq!(
472 result.get("greeting").and_then(|v| v.as_str()),
473 Some("Hello Alice")
474 );
475 assert_eq!(
476 result
477 .get("nested")
478 .and_then(|v| v.get("message"))
479 .and_then(|v| v.as_str()),
480 Some("Welcome Alice")
481 );
482 }
483
484 #[test]
485 fn test_substitute_in_array() {
486 let mut params = HashMap::new();
487 params.insert("item".to_string(), json!("apple"));
488
489 let input = json!(["${item}", "banana", "${item}"]);
490
491 let result = ParameterSubstitutor::substitute(&input, ¶ms);
492 assert!(result.is_ok());
493
494 let result = result.unwrap();
495 assert_eq!(result.as_array().map(|a| a.len()), Some(3));
496 }
497
498 #[test]
499 fn test_substitute_missing_parameter() {
500 let params = HashMap::new();
501
502 let result = ParameterSubstitutor::substitute(&json!("Hello ${name}"), ¶ms);
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_substitute_number_parameter() {
508 let mut params = HashMap::new();
509 params.insert("count".to_string(), json!(42));
510
511 let result = ParameterSubstitutor::substitute(&json!("Count: ${count}"), ¶ms);
512 assert!(result.is_ok());
513 assert_eq!(result.unwrap(), json!("Count: 42"));
514 }
515
516 #[test]
517 fn test_substitute_boolean_parameter() {
518 let mut params = HashMap::new();
519 params.insert("enabled".to_string(), json!(true));
520
521 let result = ParameterSubstitutor::substitute(&json!("Enabled: ${enabled}"), ¶ms);
522 assert!(result.is_ok());
523 assert_eq!(result.unwrap(), json!("Enabled: true"));
524 }
525
526 #[test]
527 fn test_substitute_nested_references() {
528 let mut params = HashMap::new();
529 params.insert("greeting".to_string(), json!("Hello"));
530 params.insert("name".to_string(), json!("${greeting} Alice"));
531
532 let result = ParameterSubstitutor::substitute(&json!("${name}"), ¶ms);
534 assert!(result.is_ok());
535 assert_eq!(result.unwrap(), json!("Hello Alice"));
537 }
538
539 #[test]
540 fn test_substitute_invalid_parameter_name() {
541 let params = HashMap::new();
542
543 let result = ParameterSubstitutor::substitute(&json!("Hello ${na@me}"), ¶ms);
544 assert!(result.is_err());
545 }
546}