// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #![allow(missing_debug_implementations)] // validator is zero-sized marker #![allow(dead_code)] #![allow(clippy::pattern_type_mismatch, clippy::needless_continue)] use crate::{ schema::{error::ValidationError, Schema, Type}, value::Object, *, }; use alloc::collections::BTreeMap; use regex::Regex; type String = Rc; /// Validator for checking if a Value conforms to a Schema. pub struct SchemaValidator; impl SchemaValidator { /// Validates a Value against a Schema. /// /// # Arguments /// * `value` - The Value to validate /// * `schema` - The Schema to validate against /// /// # Returns /// * `Ok(())` if the value conforms to the schema /// * `Err(ValidationError)` if validation fails /// /// # Example /// ```rust /// use regorus::schema::{Schema, validate::SchemaValidator}; /// use regorus::Value; /// use serde_json::json; /// /// let schema_json = json!({ /// "type": "string", /// "minLength": 1, /// "maxLength": 10 /// }); /// let schema = Schema::from_serde_json_value(schema_json).unwrap(); /// let value = Value::from("hello"); /// /// let result = SchemaValidator::validate(&value, &schema); /// assert!(result.is_ok()); /// ``` pub fn validate(value: &Value, schema: &Schema) -> Result<(), ValidationError> { Self::validate_with_path(value, schema, "") } /// Internal validation function that tracks the current path for error reporting. fn validate_with_path( value: &Value, schema: &Schema, path: &str, ) -> Result<(), ValidationError> { match schema.as_type() { Type::Any { .. } => { // Any type accepts all values Ok(()) } Type::Integer { minimum, maximum, .. } => Self::validate_integer(value, *minimum, *maximum, path), Type::Number { minimum, maximum, .. } => Self::validate_number(value, *minimum, *maximum, path), Type::Boolean { .. } => Self::validate_boolean(value, path), Type::Null { .. } => Self::validate_null(value, path), Type::String { min_length, max_length, pattern, .. } => Self::validate_string(value, *min_length, *max_length, pattern.as_ref(), path), Type::Array { items, min_items, max_items, .. } => Self::validate_array(value, items, *min_items, *max_items, path), Type::Object { properties, required, additional_properties, discriminated_subobject, .. } => Self::validate_object( value, properties, required.as_ref().map(|r| &**r), additional_properties.as_ref(), discriminated_subobject.as_ref().map(|d| &**d), path, ), Type::AnyOf(schemas) => Self::validate_any_of(value, schemas, path), Type::Const { value: const_value, .. } => Self::validate_const(value, const_value, path), Type::Enum { values, .. } => Self::validate_enum(value, values, path), Type::Set { items, .. } => Self::validate_set(value, items, path), } } fn validate_integer( value: &Value, minimum: Option, maximum: Option, path: &str, ) -> Result<(), ValidationError> { match value { Value::Number(num) => { if let Some(int_val) = num.as_i64() { if let Some(min) = minimum { if int_val < min { return Err(ValidationError::OutOfRange { value: int_val.to_string().into(), min: Some(min.to_string().into()), max: maximum.map(|m| m.to_string().into()), path: path.to_string().into(), }); } } if let Some(max) = maximum { if int_val > max { return Err(ValidationError::OutOfRange { value: int_val.to_string().into(), min: minimum.map(|m| m.to_string().into()), max: Some(max.to_string().into()), path: path.into(), }); } } Ok(()) } else { Err(ValidationError::TypeMismatch { expected: "integer".into(), actual: "non-integer number".into(), path: path.into(), }) } } _ => Err(ValidationError::TypeMismatch { expected: "integer".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_number( value: &Value, minimum: Option, maximum: Option, path: &str, ) -> Result<(), ValidationError> { match value { Value::Number(num) => { if let Some(float_val) = num.as_f64() { if let Some(min) = minimum { if float_val < min { return Err(ValidationError::OutOfRange { value: float_val.to_string().into(), min: Some(min.to_string().into()), max: maximum.map(|m| m.to_string().into()), path: path.into(), }); } } if let Some(max) = maximum { if float_val > max { return Err(ValidationError::OutOfRange { value: float_val.to_string().into(), min: minimum.map(|m| m.to_string().into()), max: Some(max.to_string().into()), path: path.to_string().into(), }); } } Ok(()) } else { Err(ValidationError::TypeMismatch { expected: "number".into(), actual: "non-numeric value".into(), path: path.into(), }) } } _ => Err(ValidationError::TypeMismatch { expected: "number".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_boolean(value: &Value, path: &str) -> Result<(), ValidationError> { match value { Value::Bool(_) => Ok(()), _ => Err(ValidationError::TypeMismatch { expected: "boolean".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_null(value: &Value, path: &str) -> Result<(), ValidationError> { match value { Value::Null => Ok(()), _ => Err(ValidationError::TypeMismatch { expected: "null".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_string( value: &Value, min_length: Option, max_length: Option, pattern: Option<&String>, path: &str, ) -> Result<(), ValidationError> { match value { Value::String(string_value) => { let str_len = string_value.len(); // Check length constraints if let Some(min) = min_length { if str_len < min { return Err(ValidationError::LengthConstraint { actual_length: str_len, min_length: Some(min), max_length, path: path.into(), }); } } if let Some(max) = max_length { if str_len > max { return Err(ValidationError::LengthConstraint { actual_length: str_len, min_length, max_length: Some(max), path: path.into(), }); } } // Check pattern constraint if let Some(pattern_str) = pattern { let regex = Regex::new(pattern_str).map_err(|e| ValidationError::InvalidPattern { pattern: pattern_str.as_ref().into(), error: e.to_string().into(), })?; if !regex.is_match(string_value) { return Err(ValidationError::PatternMismatch { value: string_value.to_string().into(), pattern: pattern_str.clone(), path: path.into(), }); } } Ok(()) } _ => Err(ValidationError::TypeMismatch { expected: "string".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_array( value: &Value, items_schema: &Schema, min_items: Option, max_items: Option, path: &str, ) -> Result<(), ValidationError> { match value { Value::Array(array_value) => { let arr_len = array_value.len(); // Check size constraints if let Some(min) = min_items { if arr_len < min { return Err(ValidationError::ArraySizeConstraint { actual_size: arr_len, min_items: Some(min), max_items, path: path.into(), }); } } if let Some(max) = max_items { if arr_len > max { return Err(ValidationError::ArraySizeConstraint { actual_size: arr_len, min_items, max_items: Some(max), path: path.into(), }); } } // Validate each item for (index, item) in array_value.iter().enumerate() { Self::validate_with_path( item, items_schema, &if path.is_empty() { format!("[{index}]") } else { format!("{path}[{index}]") }, ) .map_err(|e| { ValidationError::ArrayItemValidationFailed { index, path: path.into(), error: Box::new(e), } })?; } Ok(()) } _ => Err(ValidationError::TypeMismatch { expected: "array".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_object( value: &Value, properties: &BTreeMap, required: Option<&Vec>, additional_properties: Option<&Schema>, discriminated_subobject: Option<&crate::schema::DiscriminatedSubobject>, path: &str, ) -> Result<(), ValidationError> { match value { Value::Object(object_value) => { // Check required properties if let Some(required_props) = required { for required_prop in required_props.iter() { if !object_value.contains_key(&Value::String(required_prop.clone())) { return Err(ValidationError::MissingRequiredProperty { property: required_prop.clone(), path: path.into(), }); } } } // Handle discriminated subobjects (allOf with if/then) // Validates against the appropriate variant schema based on discriminator field value if let Some(discriminated_subobject) = discriminated_subobject { Self::validate_discriminated_subobject_with_base( object_value, discriminated_subobject, properties, additional_properties, path, )?; } else { // Only validate regular object properties if no discriminated subobject exists // Validate each property for (prop_name, prop_value) in object_value.iter() { // First, ensure the property key is a string let prop_name_str = match prop_name { Value::String(string_key) => string_key, _ => { return Err(ValidationError::NonStringKey { key_type: Self::value_type_name(prop_name), path: path.into(), }); } }; // Create property path lazily using a closure let make_prop_path = || { if path.is_empty() { format!("[{prop_name_str}]") } else { format!("{path}.{prop_name_str}") } }; if let Some(prop_schema) = properties.get(prop_name_str) { // Property is defined in schema, validate against it Self::validate_with_path(prop_value, prop_schema, &make_prop_path()) .map_err(|e| ValidationError::PropertyValidationFailed { property: prop_name_str.clone(), path: path.into(), error: Box::new(e), })?; } else if let Some(additional_schema) = additional_properties { // Property is not defined but additional properties are allowed Self::validate_with_path( prop_value, additional_schema, &make_prop_path(), ) .map_err(|e| { ValidationError::PropertyValidationFailed { property: prop_name_str.clone(), path: path.into(), error: Box::new(e), } })?; } else { // Property is not defined and additional properties are not allowed return Err(ValidationError::AdditionalPropertiesNotAllowed { property: prop_name_str.clone(), path: path.into(), }); } } } Ok(()) } _ => Err(ValidationError::TypeMismatch { expected: "object".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_any_of( value: &Value, schemas: &Vec, path: &str, ) -> Result<(), ValidationError> { let mut errors = Vec::new(); for schema in schemas { match Self::validate_with_path(value, schema, path) { Ok(()) => return Ok(()), // If any schema matches, validation succeeds Err(e) => errors.push(e), } } // If no schema matched, return error with all validation attempts Err(ValidationError::NoUnionMatch { path: path.into(), errors, }) } fn validate_const( value: &Value, const_value: &Value, path: &str, ) -> Result<(), ValidationError> { if value == const_value { Ok(()) } else { let expected_json = serde_json::to_string(const_value).unwrap_or_else(|_| format!("{const_value:?}")); let actual_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}")); Err(ValidationError::ConstMismatch { expected: expected_json.into(), actual: actual_json.into(), path: path.into(), }) } } fn validate_enum( value: &Value, allowed_values: &[Value], path: &str, ) -> Result<(), ValidationError> { if allowed_values.contains(value) { Ok(()) } else { // Convert Value to JSON string, fallback to debug format if JSON serialization fails let value_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}")); let allowed_json: Vec = allowed_values .iter() .map(|v| { serde_json::to_string(v) .unwrap_or_else(|_| format!("{v:?}")) .into() }) .collect(); Err(ValidationError::NotInEnum { value: value_json.into(), allowed_values: allowed_json, path: path.into(), }) } } fn validate_set( value: &Value, items_schema: &Schema, path: &str, ) -> Result<(), ValidationError> { match value { Value::Set(set_value) => { // Validate each item in the set for (index, item) in set_value.iter().enumerate() { Self::validate_with_path( item, items_schema, &if path.is_empty() { format!("{{{index}}}]") } else { format!("{path}{{{index}}}]") }, )?; } Ok(()) } _ => Err(ValidationError::TypeMismatch { expected: "set".into(), actual: Self::value_type_name(value), path: path.into(), }), } } fn validate_discriminated_subobject_with_base( object_value: &Object, discriminated_subobject: &crate::schema::DiscriminatedSubobject, base_properties: &BTreeMap, base_additional_properties: Option<&Schema>, path: &str, ) -> Result<(), ValidationError> { let discriminator_field = &discriminated_subobject.discriminator; let discriminator_key = Value::String(discriminator_field.clone()); // Find the discriminator field value in the object let discriminator_value = object_value.get(&discriminator_key).ok_or_else(|| { ValidationError::MissingDiscriminator { discriminator: discriminator_field.clone(), path: path.into(), } })?; // Extract the string value from the discriminator field let discriminator_str = match discriminator_value { Value::String(string_value) => string_value.as_ref(), _ => { return Err(ValidationError::TypeMismatch { expected: "string".into(), actual: Self::value_type_name(discriminator_value), path: format!("{path}.{discriminator_field}").into(), }); } }; // Find the corresponding variant schema let variant_schema = discriminated_subobject .variants .get(discriminator_str) .ok_or_else(|| ValidationError::UnknownDiscriminatorValue { discriminator: discriminator_field.clone(), value: discriminator_str.into(), allowed_values: discriminated_subobject.variants.keys().cloned().collect(), path: path.into(), })?; // Validate all properties against the appropriate schemas for (prop_name, prop_value) in object_value.iter() { // First, ensure the property key is a string let prop_name_str = match prop_name { Value::String(string_key) => string_key, _ => { return Err(ValidationError::NonStringKey { key_type: Self::value_type_name(prop_name), path: path.into(), }); } }; // Create property path lazily using a closure let make_prop_path = || { if path.is_empty() { format!("[{prop_name_str}]") } else { format!("{path}.{prop_name_str}") } }; // Check if this property is defined in the variant schema first if variant_schema.properties.get(prop_name_str).is_some() { // Validate later in subobject. continue; } // Check if this property is defined in the base schema properties if let Some(prop_schema) = base_properties.get(prop_name_str) { // Property is defined in base schema, validate against it Self::validate_with_path(prop_value, prop_schema, &make_prop_path()).map_err( |e| ValidationError::PropertyValidationFailed { property: prop_name_str.clone(), path: path.into(), error: Box::new(e), }, )?; continue; } // Check if additional properties are allowed in the variant if variant_schema.additional_properties.is_some() { // Property is not defined but additional properties are allowed in variant. // Validate later. continue; } else if let Some(base_additional) = base_additional_properties { // Check if additional properties are allowed in the base schema Self::validate_with_path(prop_value, base_additional, &make_prop_path()).map_err( |e| ValidationError::PropertyValidationFailed { property: prop_name_str.clone(), path: path.into(), error: Box::new(e), }, )?; } else { // Property is not defined and additional properties are not allowed return Err(ValidationError::AdditionalPropertiesNotAllowed { property: prop_name_str.clone(), path: path.into(), }); } } // Validate the object against the variant schema for required properties Self::validate_subobject(object_value, variant_schema, path).map_err(|e| { ValidationError::DiscriminatedSubobjectValidationFailed { discriminator: discriminator_field.clone(), value: discriminator_str.into(), path: path.into(), error: Box::new(e), } }) } fn validate_subobject( object_value: &Object, subobject: &crate::schema::Subobject, path: &str, ) -> Result<(), ValidationError> { // Check required properties from the subobject if let Some(required_props) = &subobject.required { for required_prop in required_props.iter() { if !object_value.contains_key(&Value::String(required_prop.clone())) { return Err(ValidationError::MissingRequiredProperty { property: required_prop.clone(), path: path.into(), }); } } } // Validate each property in the subobject for (prop_name, prop_schema) in subobject.properties.iter() { let prop_key = Value::String(prop_name.clone()); if let Some(prop_value) = object_value.get(&prop_key) { Self::validate_with_path( prop_value, prop_schema, &if path.is_empty() { format!("[{prop_name}]") } else { format!("{path}.{prop_name}") }, ) .map_err(|e| ValidationError::PropertyValidationFailed { property: prop_name.clone(), path: path.into(), error: Box::new(e), })?; } } // Handle additional properties if specified if let Some(additional_schema) = &subobject.additional_properties { for (prop_name, prop_value) in object_value.iter() { if let Value::String(prop_name_str) = prop_name { if !subobject.properties.contains_key(prop_name_str) { Self::validate_with_path( prop_value, additional_schema, &if path.is_empty() { format!("[{prop_name_str}]") } else { format!("{path}.{prop_name_str}") }, ) .map_err(|e| { ValidationError::PropertyValidationFailed { property: prop_name_str.clone(), path: path.into(), error: Box::new(e), } })?; } } } } Ok(()) } fn value_type_name(value: &Value) -> String { match value { Value::Null => "null".into(), Value::Bool(_) => "boolean".into(), Value::Number(_) => "number".into(), Value::String(_) => "string".into(), Value::Array(_) => "array".into(), Value::Set(_) => "set".into(), Value::Object(_) => "object".into(), Value::Undefined => "undefined".into(), } } }