//! Parameter validation utilities use std::collections::HashMap; use crate::types::{ParameterSchema, ParameterValues, ValidationResult, ValidationError, ParameterType}; use regex::Regex; /// Validates parameters against their schemas pub fn validate_parameters( parameters: &ParameterValues, schemas: &HashMap, ) -> ValidationResult { let mut errors = Vec::new(); // Check required parameters for (name, schema) in schemas { if schema.required.unwrap_or(false) && !parameters.contains_key(name) { errors.push(ValidationError::new( name, "Required parameter is missing", )); continue; } // Validate parameter if present if let Some(value) = parameters.get(name) { if let Err(error) = validate_parameter_value(name, value, schema) { errors.push(error); } } } // Check for unknown parameters for name in parameters.keys() { if !schemas.contains_key(name) { errors.push(ValidationError::new( name, "Unknown parameter", )); } } if errors.is_empty() { ValidationResult::success() } else { ValidationResult::failure(errors) } } /// Validates a single parameter value against its schema pub fn validate_parameter_value( name: &str, value: &serde_json::Value, schema: &ParameterSchema, ) -> Result<(), ValidationError> { // Check type match (&schema.param_type, value) { (ParameterType::String, serde_json::Value::String(s)) => { validate_string_constraints(name, s, schema)?; } (ParameterType::Number, serde_json::Value::Number(n)) => { validate_number_constraints(name, n, schema)?; } (ParameterType::Boolean, serde_json::Value::Bool(_)) => { // Boolean values are always valid } (ParameterType::Array, serde_json::Value::Array(arr)) => { validate_array_constraints(name, arr, schema)?; } (ParameterType::Object, serde_json::Value::Object(_)) => { // Object validation could be more complex, but for now just check type } _ => { return Err(ValidationError::with_value( name, format!("Expected type {:?}, got {:?}", schema.param_type, get_value_type(value)), value.clone(), )); } } // Check enum constraints if let Some(enum_values) = &schema.r#enum { if !enum_values.contains(value) { return Err(ValidationError::with_value( name, format!("Value must be one of: {enum_values:?}"), value.clone(), )); } } Ok(()) } /// Validates string constraints fn validate_string_constraints( name: &str, value: &str, schema: &ParameterSchema, ) -> Result<(), ValidationError> { // Check pattern if let Some(pattern) = &schema.pattern { let regex = Regex::new(pattern).map_err(|_| { ValidationError::new(name, "Invalid regex pattern in schema") })?; if !regex.is_match(value) { return Err(ValidationError::with_value( name, format!("String does not match pattern: {pattern}"), serde_json::Value::String(value.to_string()), )); } } // Check length constraints (using min/max as length bounds for strings) if let Some(min) = schema.min { if (value.len() as f64) < min { return Err(ValidationError::with_value( name, format!("String length must be at least {min}"), serde_json::Value::String(value.to_string()), )); } } if let Some(max) = schema.max { if (value.len() as f64) > max { return Err(ValidationError::with_value( name, format!("String length must be at most {max}"), serde_json::Value::String(value.to_string()), )); } } Ok(()) } /// Validates number constraints fn validate_number_constraints( name: &str, value: &serde_json::Number, schema: &ParameterSchema, ) -> Result<(), ValidationError> { let num_value = value.as_f64().unwrap_or(0.0); if let Some(min) = schema.min { if num_value < min { return Err(ValidationError::with_value( name, format!("Number must be at least {min}"), serde_json::Value::Number(value.clone()), )); } } if let Some(max) = schema.max { if num_value > max { return Err(ValidationError::with_value( name, format!("Number must be at most {max}"), serde_json::Value::Number(value.clone()), )); } } Ok(()) } /// Validates array constraints fn validate_array_constraints( name: &str, value: &[serde_json::Value], schema: &ParameterSchema, ) -> Result<(), ValidationError> { // Check length constraints if let Some(min) = schema.min { if (value.len() as f64) < min { return Err(ValidationError::with_value( name, format!("Array length must be at least {min}"), serde_json::Value::Array(value.to_vec()), )); } } if let Some(max) = schema.max { if (value.len() as f64) > max { return Err(ValidationError::with_value( name, format!("Array length must be at most {max}"), serde_json::Value::Array(value.to_vec()), )); } } // Validate items if schema is provided if let Some(items_schema) = &schema.items { for (index, item) in value.iter().enumerate() { let item_name = format!("{name}[{index}]"); validate_parameter_value(&item_name, item, items_schema)? } } Ok(()) } /// Gets the type of a JSON value fn get_value_type(value: &serde_json::Value) -> &'static str { match value { serde_json::Value::Null => "null", serde_json::Value::Bool(_) => "boolean", serde_json::Value::Number(_) => "number", serde_json::Value::String(_) => "string", serde_json::Value::Array(_) => "array", serde_json::Value::Object(_) => "object", } } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_validate_required_parameter() { let mut schemas = HashMap::new(); schemas.insert("test".to_string(), ParameterSchema { param_type: ParameterType::String, required: Some(true), default: None, description: None, r#enum: None, min: None, max: None, pattern: None, items: None, properties: None, }); let parameters = HashMap::new(); let result = validate_parameters(¶meters, &schemas); assert!(!result.valid); assert_eq!(result.errors.len(), 1); assert_eq!(result.errors[0].path, "test"); } #[test] fn test_validate_string_parameter() { let mut schemas = HashMap::new(); schemas.insert("test".to_string(), ParameterSchema { param_type: ParameterType::String, required: Some(true), default: None, description: None, r#enum: None, min: Some(3.0), max: Some(10.0), pattern: None, items: None, properties: None, }); let mut parameters = HashMap::new(); parameters.insert("test".to_string(), json!("hello")); let result = validate_parameters(¶meters, &schemas); assert!(result.valid); } }