ricecoder_workflows/
parameters.rs

1//! Parameter parsing, validation, and substitution for workflows
2
3use crate::error::{WorkflowError, WorkflowResult};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Parameter definition with optional default value
8#[derive(Debug, Clone)]
9pub struct ParameterDef {
10    /// Parameter name
11    pub name: String,
12    /// Parameter type (string, number, boolean, object, array)
13    pub param_type: ParameterType,
14    /// Default value if not provided
15    pub default: Option<Value>,
16    /// Whether the parameter is required
17    pub required: bool,
18}
19
20/// Parameter type
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ParameterType {
23    /// String parameter
24    String,
25    /// Number parameter
26    Number,
27    /// Boolean parameter
28    Boolean,
29    /// Object parameter
30    Object,
31    /// Array parameter
32    Array,
33}
34
35impl ParameterType {
36    /// Check if a value matches this parameter type
37    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
48/// Parameter validator and parser
49pub struct ParameterValidator;
50
51impl ParameterValidator {
52    /// Validate parameter definitions
53    ///
54    /// Ensures:
55    /// - Parameter names are unique
56    /// - Default values match parameter types
57    /// - Required parameters don't have defaults
58    pub fn validate_definitions(params: &[ParameterDef]) -> WorkflowResult<()> {
59        let mut seen_names = std::collections::HashSet::new();
60
61        for param in params {
62            // Check for duplicate names
63            if !seen_names.insert(&param.name) {
64                return Err(WorkflowError::Invalid(format!(
65                    "Duplicate parameter name: {}",
66                    param.name
67                )));
68            }
69
70            // Validate parameter name
71            if param.name.is_empty() {
72                return Err(WorkflowError::Invalid(
73                    "Parameter name cannot be empty".to_string(),
74                ));
75            }
76
77            // Check that parameter name is valid (alphanumeric, underscore, hyphen)
78            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            // Validate default value matches type
90            if let Some(default) = &param.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            // Required parameters should not have defaults
100            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    /// Validate provided parameter values
112    ///
113    /// Ensures:
114    /// - All required parameters are provided
115    /// - Provided values match parameter types
116    /// - No unknown parameters are provided
117    pub fn validate_values(
118        definitions: &[ParameterDef],
119        values: &HashMap<String, Value>,
120    ) -> WorkflowResult<()> {
121        // Check for unknown parameters
122        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        // Check required parameters and type matching
135        for param_def in definitions {
136            match values.get(&param_def.name) {
137                Some(value) => {
138                    // Validate type
139                    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                    // Check if required
156                    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    /// Build final parameter values with defaults
170    ///
171    /// Merges provided values with defaults, returning a complete map
172    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(&param_def.name) {
182                final_values.insert(param_def.name.clone(), value.clone());
183            } else if let Some(default) = &param_def.default {
184                final_values.insert(param_def.name.clone(), default.clone());
185            }
186        }
187
188        Ok(final_values)
189    }
190}
191
192/// Parameter substitution engine
193pub struct ParameterSubstitutor;
194
195impl ParameterSubstitutor {
196    /// Substitute parameters in a JSON value
197    ///
198    /// Replaces all parameter references (${param_name}) with their values.
199    /// Handles nested parameter references and validates all parameters are provided.
200    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    /// Substitute parameters in a string
222    ///
223    /// Replaces ${param_name} references with parameter values.
224    /// Handles nested references and validates all parameters exist.
225    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; // Prevent infinite loops
229
230        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            // Find all parameter references
240            let mut found_any = false;
241            let mut new_result = result.clone();
242
243            // Find ${...} patterns
244            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                    // Validate parameter name
252                    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                    // Get parameter value
263                    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(&params).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(&params).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(&params).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}"), &params);
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}"), &params);
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, &params);
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, &params);
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}"), &params);
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}"), &params);
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}"), &params);
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        // Nested references are recursively substituted
533        let result = ParameterSubstitutor::substitute(&json!("${name}"), &params);
534        assert!(result.is_ok());
535        // The result should have both references substituted
536        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}"), &params);
544        assert!(result.is_err());
545    }
546}