Skip to main content

oxigdal_workflow/templates/
validation.rs

1//! Template validation utilities.
2
3use crate::error::{Result, WorkflowError};
4use crate::templates::{Parameter, ParameterType, ParameterValue, WorkflowTemplate};
5use regex::Regex;
6use std::collections::HashMap;
7
8/// Template validator for validating templates and parameters.
9pub struct TemplateValidator;
10
11impl TemplateValidator {
12    /// Create a new template validator.
13    pub fn new() -> Self {
14        Self
15    }
16
17    /// Validate a workflow template.
18    pub fn validate_template(&self, template: &WorkflowTemplate) -> Result<()> {
19        // Validate basic fields
20        if template.id.is_empty() {
21            return Err(WorkflowError::validation("Template ID cannot be empty"));
22        }
23
24        if template.name.is_empty() {
25            return Err(WorkflowError::validation("Template name cannot be empty"));
26        }
27
28        if template.version.is_empty() {
29            return Err(WorkflowError::validation(
30                "Template version cannot be empty",
31            ));
32        }
33
34        // Validate version format (semantic versioning)
35        self.validate_version(&template.version)?;
36
37        // Validate parameters
38        for param in &template.parameters {
39            self.validate_parameter_definition(param)?;
40        }
41
42        // Validate template string is not empty
43        if template.workflow_template.is_empty() {
44            return Err(WorkflowError::validation(
45                "Template workflow string cannot be empty",
46            ));
47        }
48
49        // Check for duplicate parameter names
50        let mut param_names: Vec<&String> = template.parameters.iter().map(|p| &p.name).collect();
51        param_names.sort();
52        for i in 1..param_names.len() {
53            if param_names[i] == param_names[i - 1] {
54                return Err(WorkflowError::validation(format!(
55                    "Duplicate parameter name: {}",
56                    param_names[i]
57                )));
58            }
59        }
60
61        Ok(())
62    }
63
64    /// Validate parameter values against parameter definitions.
65    pub fn validate_parameters(
66        &self,
67        definitions: &[Parameter],
68        values: &HashMap<String, ParameterValue>,
69    ) -> Result<()> {
70        // Check required parameters
71        for param in definitions {
72            if param.required && !values.contains_key(&param.name) {
73                return Err(WorkflowError::validation(format!(
74                    "Required parameter '{}' is missing",
75                    param.name
76                )));
77            }
78        }
79
80        // Validate each provided value
81        for (name, value) in values {
82            if let Some(param) = definitions.iter().find(|p| p.name == *name) {
83                self.validate_parameter_value(param, value)?;
84            } else {
85                return Err(WorkflowError::validation(format!(
86                    "Unknown parameter: {}",
87                    name
88                )));
89            }
90        }
91
92        Ok(())
93    }
94
95    /// Validate a parameter definition.
96    fn validate_parameter_definition(&self, param: &Parameter) -> Result<()> {
97        if param.name.is_empty() {
98            return Err(WorkflowError::validation("Parameter name cannot be empty"));
99        }
100
101        if param.description.is_empty() {
102            return Err(WorkflowError::validation(format!(
103                "Parameter '{}' must have a description",
104                param.name
105            )));
106        }
107
108        // If not required, must have default value
109        if !param.required && param.default_value.is_none() {
110            return Err(WorkflowError::validation(format!(
111                "Optional parameter '{}' must have a default value",
112                param.name
113            )));
114        }
115
116        // Validate default value matches type
117        if let Some(default) = &param.default_value {
118            self.validate_parameter_value(param, default)?;
119        }
120
121        Ok(())
122    }
123
124    /// Validate a parameter value against its definition.
125    fn validate_parameter_value(&self, param: &Parameter, value: &ParameterValue) -> Result<()> {
126        // Check type compatibility
127        match (&param.param_type, value) {
128            (ParameterType::String, ParameterValue::String(s)) => {
129                self.validate_string_constraints(param, s)?;
130            }
131            (ParameterType::Integer, ParameterValue::Integer(i)) => {
132                self.validate_numeric_constraints(param, *i as f64)?;
133            }
134            (ParameterType::Float, ParameterValue::Float(f)) => {
135                self.validate_numeric_constraints(param, *f)?;
136            }
137            (ParameterType::Boolean, ParameterValue::Boolean(_)) => {
138                // Boolean has no constraints to validate
139            }
140            (ParameterType::Array, ParameterValue::Array(arr)) => {
141                self.validate_array_constraints(param, arr)?;
142            }
143            (ParameterType::Object, ParameterValue::Object(_)) => {
144                // Object validation could be extended
145            }
146            (ParameterType::FilePath, ParameterValue::String(s)) => {
147                self.validate_file_path(s)?;
148            }
149            (ParameterType::Url, ParameterValue::String(s)) => {
150                self.validate_url(s)?;
151            }
152            (ParameterType::Enum { allowed_values }, ParameterValue::String(s)) => {
153                if !allowed_values.contains(s) {
154                    return Err(WorkflowError::validation(format!(
155                        "Parameter '{}' value '{}' is not in allowed values: {:?}",
156                        param.name, s, allowed_values
157                    )));
158                }
159            }
160            _ => {
161                return Err(WorkflowError::validation(format!(
162                    "Parameter '{}' type mismatch: expected {:?}, got incompatible value",
163                    param.name, param.param_type
164                )));
165            }
166        }
167
168        Ok(())
169    }
170
171    /// Validate string constraints.
172    fn validate_string_constraints(&self, param: &Parameter, value: &str) -> Result<()> {
173        if let Some(constraints) = &param.constraints {
174            if let Some(min_len) = constraints.min_length {
175                if value.len() < min_len {
176                    return Err(WorkflowError::validation(format!(
177                        "Parameter '{}' length {} is less than minimum {}",
178                        param.name,
179                        value.len(),
180                        min_len
181                    )));
182                }
183            }
184
185            if let Some(max_len) = constraints.max_length {
186                if value.len() > max_len {
187                    return Err(WorkflowError::validation(format!(
188                        "Parameter '{}' length {} exceeds maximum {}",
189                        param.name,
190                        value.len(),
191                        max_len
192                    )));
193                }
194            }
195
196            if let Some(pattern) = &constraints.pattern {
197                let regex = Regex::new(pattern).map_err(|e| {
198                    WorkflowError::validation(format!("Invalid regex pattern: {}", e))
199                })?;
200
201                if !regex.is_match(value) {
202                    return Err(WorkflowError::validation(format!(
203                        "Parameter '{}' value '{}' does not match pattern '{}'",
204                        param.name, value, pattern
205                    )));
206                }
207            }
208        }
209
210        Ok(())
211    }
212
213    /// Validate numeric constraints.
214    fn validate_numeric_constraints(&self, param: &Parameter, value: f64) -> Result<()> {
215        if let Some(constraints) = &param.constraints {
216            if let Some(min) = constraints.min {
217                if value < min {
218                    return Err(WorkflowError::validation(format!(
219                        "Parameter '{}' value {} is less than minimum {}",
220                        param.name, value, min
221                    )));
222                }
223            }
224
225            if let Some(max) = constraints.max {
226                if value > max {
227                    return Err(WorkflowError::validation(format!(
228                        "Parameter '{}' value {} exceeds maximum {}",
229                        param.name, value, max
230                    )));
231                }
232            }
233        }
234
235        Ok(())
236    }
237
238    /// Validate array constraints.
239    fn validate_array_constraints(
240        &self,
241        param: &Parameter,
242        value: &[ParameterValue],
243    ) -> Result<()> {
244        if let Some(constraints) = &param.constraints {
245            if let Some(min_len) = constraints.min_length {
246                if value.len() < min_len {
247                    return Err(WorkflowError::validation(format!(
248                        "Parameter '{}' array length {} is less than minimum {}",
249                        param.name,
250                        value.len(),
251                        min_len
252                    )));
253                }
254            }
255
256            if let Some(max_len) = constraints.max_length {
257                if value.len() > max_len {
258                    return Err(WorkflowError::validation(format!(
259                        "Parameter '{}' array length {} exceeds maximum {}",
260                        param.name,
261                        value.len(),
262                        max_len
263                    )));
264                }
265            }
266        }
267
268        Ok(())
269    }
270
271    /// Validate file path.
272    fn validate_file_path(&self, _path: &str) -> Result<()> {
273        // Basic validation - could be extended
274        Ok(())
275    }
276
277    /// Validate URL.
278    fn validate_url(&self, url: &str) -> Result<()> {
279        let url_regex = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$")
280            .map_err(|e| WorkflowError::validation(format!("Invalid URL regex: {}", e)))?;
281
282        if !url_regex.is_match(url) {
283            return Err(WorkflowError::validation(format!(
284                "Invalid URL format: {}",
285                url
286            )));
287        }
288
289        Ok(())
290    }
291
292    /// Validate semantic version.
293    fn validate_version(&self, version: &str) -> Result<()> {
294        let version_regex = Regex::new(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$")
295            .map_err(|e| WorkflowError::validation(format!("Invalid version regex: {}", e)))?;
296
297        if !version_regex.is_match(version) {
298            return Err(WorkflowError::validation(format!(
299                "Invalid semantic version format: {}",
300                version
301            )));
302        }
303
304        Ok(())
305    }
306}
307
308impl Default for TemplateValidator {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::templates::{ParameterConstraints, TemplateCategory, TemplateMetadata};
318    use chrono::Utc;
319
320    #[test]
321    fn test_validate_template_basic() {
322        let mut template = WorkflowTemplate::new("test", "Test", "Description");
323        template.workflow_template = "{}".to_string();
324        template.version = "1.0.0".to_string();
325
326        let validator = TemplateValidator::new();
327        assert!(validator.validate_template(&template).is_ok());
328    }
329
330    #[test]
331    fn test_validate_empty_id() {
332        let template = WorkflowTemplate {
333            id: "".to_string(),
334            name: "Test".to_string(),
335            description: "Description".to_string(),
336            version: "1.0.0".to_string(),
337            author: "Test".to_string(),
338            tags: vec![],
339            parameters: vec![],
340            workflow_template: "{}".to_string(),
341            metadata: TemplateMetadata {
342                created_at: Utc::now(),
343                updated_at: Utc::now(),
344                category: TemplateCategory::Custom,
345                complexity: 1,
346                estimated_duration: None,
347                required_resources: vec![],
348                compatible_versions: vec![],
349            },
350            examples: vec![],
351        };
352
353        let validator = TemplateValidator::new();
354        assert!(validator.validate_template(&template).is_err());
355    }
356
357    #[test]
358    fn test_validate_version() {
359        let validator = TemplateValidator::new();
360
361        assert!(validator.validate_version("1.0.0").is_ok());
362        assert!(validator.validate_version("1.2.3").is_ok());
363        assert!(validator.validate_version("1.0.0-alpha").is_ok());
364        assert!(validator.validate_version("1.0.0+build").is_ok());
365        assert!(validator.validate_version("invalid").is_err());
366        assert!(validator.validate_version("1.0").is_err());
367    }
368
369    #[test]
370    fn test_validate_parameters() {
371        let validator = TemplateValidator::new();
372
373        let param = Parameter {
374            name: "test_param".to_string(),
375            param_type: ParameterType::Integer,
376            description: "Test parameter".to_string(),
377            required: true,
378            default_value: None,
379            constraints: Some(ParameterConstraints {
380                min: Some(0.0),
381                max: Some(100.0),
382                min_length: None,
383                max_length: None,
384                pattern: None,
385            }),
386        };
387
388        let mut values = HashMap::new();
389        values.insert("test_param".to_string(), ParameterValue::Integer(50));
390
391        assert!(validator.validate_parameters(&[param], &values).is_ok());
392    }
393
394    #[test]
395    fn test_validate_missing_required() {
396        let validator = TemplateValidator::new();
397
398        let param = Parameter {
399            name: "required_param".to_string(),
400            param_type: ParameterType::String,
401            description: "Required parameter".to_string(),
402            required: true,
403            default_value: None,
404            constraints: None,
405        };
406
407        let values = HashMap::new();
408
409        assert!(validator.validate_parameters(&[param], &values).is_err());
410    }
411
412    #[test]
413    fn test_validate_numeric_constraints() {
414        let validator = TemplateValidator::new();
415
416        let param = Parameter {
417            name: "num_param".to_string(),
418            param_type: ParameterType::Integer,
419            description: "Numeric parameter".to_string(),
420            required: false,
421            default_value: Some(ParameterValue::Integer(50)),
422            constraints: Some(ParameterConstraints {
423                min: Some(0.0),
424                max: Some(100.0),
425                min_length: None,
426                max_length: None,
427                pattern: None,
428            }),
429        };
430
431        let value = ParameterValue::Integer(150);
432        assert!(validator.validate_parameter_value(&param, &value).is_err());
433
434        let value = ParameterValue::Integer(50);
435        assert!(validator.validate_parameter_value(&param, &value).is_ok());
436    }
437
438    #[test]
439    fn test_validate_string_pattern() {
440        let validator = TemplateValidator::new();
441
442        let param = Parameter {
443            name: "email".to_string(),
444            param_type: ParameterType::String,
445            description: "Email parameter".to_string(),
446            required: false,
447            default_value: Some(ParameterValue::String("test@example.com".to_string())),
448            constraints: Some(ParameterConstraints {
449                min: None,
450                max: None,
451                min_length: None,
452                max_length: None,
453                pattern: Some(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$".to_string()),
454            }),
455        };
456
457        let value = ParameterValue::String("invalid-email".to_string());
458        assert!(validator.validate_parameter_value(&param, &value).is_err());
459
460        let value = ParameterValue::String("valid@example.com".to_string());
461        assert!(validator.validate_parameter_value(&param, &value).is_ok());
462    }
463}