| use log::debug; |
|
|
| use super::{EntitySchema, ValidationError}; |
| use crate::Entity; |
|
|
| pub type ValidationResult = Result<(), Vec<ValidationError>>; |
|
|
| impl EntitySchema { |
| |
| pub fn validate(&self, entity: &Entity) -> ValidationResult { |
| debug!( |
| "Validating entity: '{}' for schema: '{}'", |
| entity.id, self.entity_type |
| ); |
|
|
| let mut errors = Vec::new(); |
|
|
| |
| if entity.entity_type != self.entity_type { |
| errors.push(ValidationError::mismatched_entity_type( |
| &entity.id, |
| &self.entity_type, |
| &entity.entity_type, |
| )) |
| } |
|
|
| |
| for (field_name, field_schema) in &self.fields { |
| match entity.get_field(field_name) { |
| |
| Some(field_value) => { |
| let expected_type = field_schema.expected_type(); |
| if !field_value.is_type(expected_type) { |
| errors.push(ValidationError::mismatched_field_type( |
| &entity.id, |
| field_name, |
| expected_type, |
| &field_value.get_type(), |
| )); |
| } else if let crate::field::FieldValue::Enum(value) = field_value { |
| |
| if let Some(allowed_values) = field_schema.allowed_values() { |
| let normalized_value = value.trim().to_lowercase(); |
| if !allowed_values.contains(&normalized_value) { |
| errors.push(ValidationError::invalid_enum_value( |
| &entity.id, |
| field_name, |
| value, |
| allowed_values, |
| )); |
| } |
| } else { |
| errors.push(ValidationError::invalid_enum_value( |
| &entity.id, |
| field_name, |
| value, |
| &[], |
| )); |
| } |
| } |
| } |
| |
| None => { |
| if field_schema.is_required() { |
| errors.push(ValidationError::missing_field(&entity.id, field_name)); |
| } |
| } |
| } |
| } |
|
|
| if errors.is_empty() { |
| Ok(()) |
| } else { |
| debug!( |
| "Entity '{}' failed validation with {} errors", |
| entity.id, |
| errors.len() |
| ); |
| Err(errors) |
| } |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::schema::ValidationErrorType; |
| use crate::{ |
| EntityId, EntityType, FieldId, |
| field::{FieldType, FieldValue}, |
| }; |
| use assert_matches::assert_matches; |
|
|
| #[test] |
| fn test_validate_ok() { |
| let schema = EntitySchema::new(EntityType::new("person")) |
| .with_required_field(FieldId::new("name"), FieldType::String) |
| .with_optional_field(FieldId::new("email"), FieldType::String); |
|
|
| let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person")) |
| .with_field( |
| FieldId::new("name"), |
| FieldValue::String(String::from("John Doe")), |
| ); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_ok()); |
| } |
|
|
| #[test] |
| fn test_validate_error_mismatched_entity_types() { |
| let schema = EntitySchema::new(EntityType::new("test_a")); |
| let entity = Entity::new(EntityId::new("test"), EntityType::new("test_b")); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_err()); |
|
|
| let errors = result.unwrap_err(); |
| assert_eq!(errors.len(), 1); |
|
|
| assert_matches!( |
| &errors[0].error_type, |
| ValidationErrorType::MismatchedEntityType { expected, actual } if expected == &EntityType::new("test_a") && actual == &EntityType::new("test_b") |
| ); |
| } |
|
|
| #[test] |
| fn test_validate_error_missing_field() { |
| let schema = EntitySchema::new(EntityType::new("person")) |
| .with_required_field(FieldId::new("name"), FieldType::String) |
| .with_required_field(FieldId::new("email"), FieldType::String); |
|
|
| let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person")) |
| .with_field( |
| FieldId::new("name"), |
| FieldValue::String(String::from("John Doe")), |
| ); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_err()); |
|
|
| let errors = result.unwrap_err(); |
| assert_eq!(errors.len(), 1); |
|
|
| assert_matches!( |
| &errors[0].error_type, |
| ValidationErrorType::MissingRequiredField { required } if required == &FieldId::new("email") |
| ); |
| } |
|
|
| #[test] |
| fn test_validate_error_mismatched_field_types() { |
| let schema = EntitySchema::new(EntityType::new("person")) |
| .with_required_field(FieldId::new("is_nice"), FieldType::Boolean); |
|
|
| let entity = Entity::new(EntityId::new("test_person"), EntityType::new("person")) |
| .with_field( |
| FieldId::new("is_nice"), |
| FieldValue::String("Sure".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_err()); |
|
|
| let errors = result.unwrap_err(); |
| assert_eq!(errors.len(), 1); |
|
|
| assert_matches!( |
| &errors[0].error_type, |
| ValidationErrorType::MismatchedFieldType { expected, actual } if expected == &FieldType::Boolean && actual == &FieldType::String |
| ); |
| } |
|
|
| #[test] |
| fn test_validate_enum_with_valid_value() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_required_enum( |
| FieldId::new("status"), |
| vec![ |
| "prospect".to_string(), |
| "customer".to_string(), |
| "partner".to_string(), |
| ], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")) |
| .with_field( |
| FieldId::new("status"), |
| FieldValue::Enum("customer".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
| assert!(result.is_ok()); |
| } |
|
|
| #[test] |
| fn test_validate_enum_with_case_insensitive_match() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_required_enum( |
| FieldId::new("status"), |
| vec!["prospect".to_string(), "customer".to_string()], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")) |
| .with_field( |
| FieldId::new("status"), |
| FieldValue::Enum("CUSTOMER".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
| assert!(result.is_ok()); |
| } |
|
|
| #[test] |
| fn test_validate_enum_with_whitespace_trimmed() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_required_enum( |
| FieldId::new("status"), |
| vec!["prospect".to_string(), "customer".to_string()], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")) |
| .with_field( |
| FieldId::new("status"), |
| FieldValue::Enum(" customer ".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
| assert!(result.is_ok()); |
| } |
|
|
| #[test] |
| fn test_validate_enum_with_invalid_value() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_required_enum( |
| FieldId::new("status"), |
| vec![ |
| "prospect".to_string(), |
| "customer".to_string(), |
| "partner".to_string(), |
| ], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")) |
| .with_field( |
| FieldId::new("status"), |
| FieldValue::Enum("client".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_err()); |
|
|
| let errors = result.unwrap_err(); |
| assert_eq!(errors.len(), 1); |
|
|
| assert_matches!( |
| &errors[0].error_type, |
| ValidationErrorType::InvalidEnumValue { actual, allowed } |
| if actual == "client" && allowed == &vec!["prospect".to_string(), "customer".to_string(), "partner".to_string()] |
| ); |
| } |
|
|
| #[test] |
| fn test_validate_optional_enum_can_be_missing() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_optional_enum( |
| FieldId::new("status"), |
| vec!["prospect".to_string(), "customer".to_string()], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")); |
|
|
| let result = schema.validate(&entity); |
| assert!(result.is_ok()); |
| } |
|
|
| #[test] |
| fn test_validate_optional_enum_validates_when_present() { |
| let schema = EntitySchema::new(EntityType::new("account")).with_optional_enum( |
| FieldId::new("status"), |
| vec!["prospect".to_string(), "customer".to_string()], |
| ); |
|
|
| let entity = Entity::new(EntityId::new("test_account"), EntityType::new("account")) |
| .with_field( |
| FieldId::new("status"), |
| FieldValue::Enum("invalid".to_string()), |
| ); |
|
|
| let result = schema.validate(&entity); |
|
|
| assert!(result.is_err()); |
|
|
| let errors = result.unwrap_err(); |
| assert_eq!(errors.len(), 1); |
|
|
| assert_matches!( |
| &errors[0].error_type, |
| ValidationErrorType::InvalidEnumValue { actual, .. } if actual == "invalid" |
| ); |
| } |
| } |
|
|