pub struct ValidationContextCore;
impl ValidationContextCore {
pub fn fail_type_mismatch(expected: &SchemaType, node: &Node) -> ValidationError {
ValidationError::TypeMismatch {
expected: format!("{:?}", expected),
found: node.to_string_lossy(),
}
}
pub fn fail_range(value: f64, min: Option<f64>, max: Option<f64>) -> ValidationError {
ValidationError::RangeError { value, min, max }
}
pub fn fail_required(field: &str) -> ValidationError {
ValidationError::RequiredFieldMissing {
field: field.to_string(),
}
}
}
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use log::warn;
use crate::error::YamlError;
use crate::nodes::node::Node;
use crate::nodes::node::NodeStringConvert;
use crate::validation::error::{ValidationError, ValidationIssue};
use crate::validation::schema::{PropertySchema, Schema, SchemaType};
use crate::validation::validators::{
EnumValidator, LengthValidator, PatternValidator, RangeValidator, TypeValidator, Validator,
};
#[derive(Debug, Clone)]
pub struct ValidationContext {
path: Vec<String>,
errors: Vec<ValidationIssue>,
fail_fast: bool,
}
impl ValidationContext {
pub fn new() -> Self {
Self {
path: Vec::new(),
errors: Vec::new(),
fail_fast: false,
}
}
pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
self.fail_fast = fail_fast;
self
}
fn push(&mut self, segment: impl Into<String>) {
self.path.push(segment.into());
}
fn pop(&mut self) {
self.path.pop();
}
fn add_error(&mut self, error: ValidationError) {
let issue = ValidationIssue::new(&self.path, error);
warn!("Validation error: {:?}", issue);
self.errors.push(issue);
}
fn should_stop(&self) -> bool {
self.fail_fast && !self.errors.is_empty()
}
pub fn errors(&self) -> &[ValidationIssue] {
&self.errors
}
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
}
impl Default for ValidationContext {
fn default() -> Self {
Self::new()
}
}
pub struct SchemaValidator {
schema: Schema,
}
impl SchemaValidator {
pub fn new(schema: Schema) -> Self {
Self { schema }
}
pub fn validate(&self, node: &Node) -> Result<(), YamlError> {
let mut ctx = ValidationContext::new();
self.validate_with_context(node, &mut ctx);
if ctx.is_valid() {
Ok(())
} else {
let msg = ctx
.errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
Err(YamlError::new(
crate::error::ErrorKind::ValidationError,
msg,
))
}
}
pub fn validate_with_context(&self, node: &Node, ctx: &mut ValidationContext) {
self.validate_property(node, &self.schema.root, ctx);
}
fn validate_property(&self, node: &Node, schema: &PropertySchema, ctx: &mut ValidationContext) {
if ctx.should_stop() {
return;
}
let type_validator = TypeValidator::new(schema.schema_type.clone());
if let Err(err) = type_validator.validate(node) {
ctx.add_error(err);
return;
}
if let (Some(min), Some(max)) = (schema.minimum, schema.maximum) {
let validator = RangeValidator::new(Some(min), Some(max));
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
} else if let Some(min) = schema.minimum {
let validator = RangeValidator::new(Some(min), None);
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
} else if let Some(max) = schema.maximum {
let validator = RangeValidator::new(None, Some(max));
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
}
if let (Some(min), Some(max)) = (schema.min_length, schema.max_length) {
let validator = LengthValidator::new(Some(min), Some(max));
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
} else if let Some(min) = schema.min_length {
let validator = LengthValidator::new(Some(min), None);
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
} else if let Some(max) = schema.max_length {
let validator = LengthValidator::new(None, Some(max));
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
}
if let Some(ref pattern) = schema.pattern {
let validator = PatternValidator::new(pattern.clone());
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
}
if let Some(ref allowed) = schema.enum_values {
let validator = EnumValidator::new(allowed.clone());
if let Err(err) = validator.validate(node) {
ctx.add_error(err);
return;
}
}
match (&schema.schema_type, node) {
(SchemaType::Array, Node::Array(arr)) => {
if let Some(ref items) = schema.items {
for (i, item) in arr.iter().enumerate() {
ctx.push(format!("[{}]", i));
self.validate_property(item, items, ctx);
ctx.pop();
if ctx.should_stop() {
return;
}
}
}
}
(SchemaType::Object, Node::Mapping(pairs)) => {
if let Some(ref properties) = schema.properties {
let mut props = BTreeMap::new();
for (key, value) in pairs {
if let Node::Str(k, _, _) = key {
props.insert(k.as_str(), value);
}
}
for (prop_name, prop_schema) in properties {
if let Some(value) = props.get(prop_name.as_str()) {
ctx.push(prop_name.clone());
self.validate_property(value, prop_schema, ctx);
ctx.pop();
if ctx.should_stop() {
return;
}
} else if prop_schema.required {
ctx.add_error(ValidationError::RequiredFieldMissing {
field: prop_name.clone(),
});
}
}
}
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nodes::node::Numeric;
use crate::validation::schema::Schema;
#[test]
fn test_simple_validation() {
let schema = Schema::string();
let validator = SchemaValidator::new(schema);
assert!(validator.validate(&Node::from("hello")).is_ok());
assert!(validator.validate(&Node::from(42)).is_err());
}
#[test]
fn test_range_validation() {
let schema = Schema {
root: PropertySchema::new(SchemaType::Integer)
.with_minimum(0.0)
.with_maximum(100.0),
title: None,
description: None,
};
let validator = SchemaValidator::new(schema);
assert!(
validator
.validate(&Node::Number(Numeric::Integer(50)))
.is_ok()
);
assert!(
validator
.validate(&Node::Number(Numeric::Integer(150)))
.is_err()
);
}
#[test]
fn test_object_validation() {
let mut properties = BTreeMap::new();
properties.insert(
"name".to_string(),
PropertySchema::new(SchemaType::String).required(),
);
properties.insert("age".to_string(), PropertySchema::new(SchemaType::Integer));
let schema = Schema::object(properties);
let validator = SchemaValidator::new(schema);
let valid_obj = Node::Mapping(vec![
(Node::from("name"), Node::from("Alice")),
(Node::from("age"), Node::Number(Numeric::Integer(30))),
]);
assert!(validator.validate(&valid_obj).is_ok());
let invalid_obj = Node::Mapping(vec![(
Node::from("age"),
Node::Number(Numeric::Integer(30)),
)]);
assert!(validator.validate(&invalid_obj).is_err());
}
#[test]
fn test_array_validation() {
let schema = Schema::array(PropertySchema::new(SchemaType::Integer));
let validator = SchemaValidator::new(schema);
let valid_arr = Node::Array(vec![
Node::Number(Numeric::Integer(1)),
Node::Number(Numeric::Integer(2)),
Node::Number(Numeric::Integer(3)),
]);
assert!(validator.validate(&valid_arr).is_ok());
let wrong_type = Node::Array(vec![Node::from("not a number")]);
assert!(validator.validate(&wrong_type).is_err());
}
#[test]
fn test_validation_error_paths() {
let mut user_props = BTreeMap::new();
user_props.insert("name".to_string(), PropertySchema::new(SchemaType::String));
user_props.insert("age".to_string(), PropertySchema::new(SchemaType::Integer));
let mut root_props = BTreeMap::new();
root_props.insert(
"user".to_string(),
PropertySchema::new(SchemaType::Object).with_properties(user_props),
);
let schema = Schema::object(root_props);
let validator = SchemaValidator::new(schema);
let obj = Node::Mapping(vec![(
Node::from("user"),
Node::Mapping(vec![
(Node::from("name"), Node::from("Alice")),
(Node::from("age"), Node::from("not a number")),
]),
)]);
let result = validator.validate(&obj);
assert!(result.is_err());
let error = result.unwrap_err();
let msg = error.to_string();
assert!(msg.contains("Type mismatch"));
}
}
#[cfg(test)]
mod additional_validation_engine_tests {
use super::*;
use crate::nodes::node::{Node, Numeric};
use crate::validation::schema::{PropertySchema, Schema, SchemaType};
#[test]
fn test_fail_type_mismatch_error() {
let err = ValidationContextCore::fail_type_mismatch(&SchemaType::String, &Node::from(42));
match err {
ValidationError::TypeMismatch { expected, found } => {
assert!(expected.contains("String"));
assert!(found.contains("42"));
}
_ => panic!("Expected TypeMismatch error"),
}
}
#[test]
fn test_fail_range_error() {
let err = ValidationContextCore::fail_range(5.0, Some(1.0), Some(10.0));
match err {
ValidationError::RangeError { value, min, max } => {
assert_eq!(value, 5.0);
assert_eq!(min, Some(1.0));
assert_eq!(max, Some(10.0));
}
_ => panic!("Expected RangeError"),
}
}
#[test]
fn test_fail_required_error() {
let err = ValidationContextCore::fail_required("foo");
match err {
ValidationError::RequiredFieldMissing { field } => {
assert_eq!(field, "foo");
}
_ => panic!("Expected RequiredFieldMissing error"),
}
}
#[test]
fn test_validation_context_fail_fast() {
let mut ctx = ValidationContext::new().with_fail_fast(true);
ctx.add_error(ValidationError::RequiredFieldMissing {
field: "x".to_string(),
});
assert!(ctx.should_stop());
}
#[test]
fn test_schema_validator_empty_object() {
let schema = Schema::object(BTreeMap::new());
let validator = SchemaValidator::new(schema);
let node = Node::Mapping(vec![]);
assert!(validator.validate(&node).is_ok());
}
#[test]
fn test_schema_validator_array_with_items() {
let mut item_schema = PropertySchema::new(SchemaType::Integer);
item_schema.required = true;
let mut root_schema = PropertySchema::new(SchemaType::Array);
root_schema.items = Some(Box::new(item_schema));
let schema = Schema {
root: root_schema,
title: None,
description: None,
};
let validator = SchemaValidator::new(schema);
let node = Node::Array(vec![
Node::Number(Numeric::Integer(1)),
Node::Number(Numeric::Integer(2)),
]);
assert!(validator.validate(&node).is_ok());
}
#[test]
fn test_schema_validator_object_missing_required() {
let mut props = BTreeMap::new();
let mut required_schema = PropertySchema::new(SchemaType::String);
required_schema.required = true;
props.insert("foo".to_string(), required_schema);
let schema = Schema::object(props);
let validator = SchemaValidator::new(schema);
let node = Node::Mapping(vec![]);
let result = validator.validate(&node);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("Required field"));
}
}