oxigdal_workflow/templates/
validation.rs1use crate::error::{Result, WorkflowError};
4use crate::templates::{Parameter, ParameterType, ParameterValue, WorkflowTemplate};
5use regex::Regex;
6use std::collections::HashMap;
7
8pub struct TemplateValidator;
10
11impl TemplateValidator {
12 pub fn new() -> Self {
14 Self
15 }
16
17 pub fn validate_template(&self, template: &WorkflowTemplate) -> Result<()> {
19 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 self.validate_version(&template.version)?;
36
37 for param in &template.parameters {
39 self.validate_parameter_definition(param)?;
40 }
41
42 if template.workflow_template.is_empty() {
44 return Err(WorkflowError::validation(
45 "Template workflow string cannot be empty",
46 ));
47 }
48
49 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 pub fn validate_parameters(
66 &self,
67 definitions: &[Parameter],
68 values: &HashMap<String, ParameterValue>,
69 ) -> Result<()> {
70 for param in definitions {
72 if param.required && !values.contains_key(¶m.name) {
73 return Err(WorkflowError::validation(format!(
74 "Required parameter '{}' is missing",
75 param.name
76 )));
77 }
78 }
79
80 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 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 !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 if let Some(default) = ¶m.default_value {
118 self.validate_parameter_value(param, default)?;
119 }
120
121 Ok(())
122 }
123
124 fn validate_parameter_value(&self, param: &Parameter, value: &ParameterValue) -> Result<()> {
126 match (¶m.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 }
140 (ParameterType::Array, ParameterValue::Array(arr)) => {
141 self.validate_array_constraints(param, arr)?;
142 }
143 (ParameterType::Object, ParameterValue::Object(_)) => {
144 }
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 fn validate_string_constraints(&self, param: &Parameter, value: &str) -> Result<()> {
173 if let Some(constraints) = ¶m.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 fn validate_numeric_constraints(&self, param: &Parameter, value: f64) -> Result<()> {
215 if let Some(constraints) = ¶m.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 fn validate_array_constraints(
240 &self,
241 param: &Parameter,
242 value: &[ParameterValue],
243 ) -> Result<()> {
244 if let Some(constraints) = ¶m.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 fn validate_file_path(&self, _path: &str) -> Result<()> {
273 Ok(())
275 }
276
277 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 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(¶m, &value).is_err());
433
434 let value = ParameterValue::Integer(50);
435 assert!(validator.validate_parameter_value(¶m, &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(¶m, &value).is_err());
459
460 let value = ParameterValue::String("valid@example.com".to_string());
461 assert!(validator.validate_parameter_value(¶m, &value).is_ok());
462 }
463}