use crate::scalar::{ScalarType, ScalarValue};
use crate::yaml::{Document, Mapping, Scalar, Sequence, TaggedNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationErrorKind {
TypeNotAllowed {
found_type: ScalarType,
allowed_types: Vec<ScalarType>,
},
CustomConstraintFailed {
constraint_name: String,
actual_value: String,
},
CoercionFailed {
from_type: ScalarType,
to_types: Vec<ScalarType>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub kind: ValidationErrorKind,
pub path: String,
pub schema_name: String,
}
impl ValidationError {
pub fn type_not_allowed(
path: impl Into<String>,
schema_name: impl Into<String>,
found_type: ScalarType,
allowed_types: Vec<ScalarType>,
) -> Self {
Self {
kind: ValidationErrorKind::TypeNotAllowed {
found_type,
allowed_types,
},
path: path.into(),
schema_name: schema_name.into(),
}
}
pub fn custom_constraint_failed(
path: impl Into<String>,
schema_name: impl Into<String>,
constraint_name: impl Into<String>,
actual_value: impl Into<String>,
) -> Self {
Self {
kind: ValidationErrorKind::CustomConstraintFailed {
constraint_name: constraint_name.into(),
actual_value: actual_value.into(),
},
path: path.into(),
schema_name: schema_name.into(),
}
}
pub fn coercion_failed(
path: impl Into<String>,
schema_name: impl Into<String>,
from_type: ScalarType,
to_types: Vec<ScalarType>,
) -> Self {
Self {
kind: ValidationErrorKind::CoercionFailed {
from_type,
to_types,
},
path: path.into(),
schema_name: schema_name.into(),
}
}
pub fn message(&self) -> String {
match &self.kind {
ValidationErrorKind::TypeNotAllowed {
found_type,
allowed_types,
} => {
format!(
"type {:?} not allowed in {} schema, expected one of {:?}",
found_type, self.schema_name, allowed_types
)
}
ValidationErrorKind::CustomConstraintFailed {
constraint_name,
actual_value,
} => {
format!(
"custom constraint '{}' failed for value '{}'",
constraint_name, actual_value
)
}
ValidationErrorKind::CoercionFailed {
from_type,
to_types,
} => {
format!(
"cannot coerce {:?} to any of {:?} in {} schema",
from_type, to_types, self.schema_name
)
}
}
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Validation error at {}: {}", self.path, self.message())
}
}
impl std::error::Error for ValidationError {}
pub type ValidationResult<T> = Result<T, Vec<ValidationError>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CustomValidationResult {
Valid,
Invalid {
constraint: String,
reason: String,
},
}
impl CustomValidationResult {
pub fn invalid(constraint: impl Into<String>, reason: impl Into<String>) -> Self {
Self::Invalid {
constraint: constraint.into(),
reason: reason.into(),
}
}
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
pub fn is_invalid(&self) -> bool {
matches!(self, Self::Invalid { .. })
}
}
pub type CustomValidator = Box<dyn Fn(&str, &str) -> CustomValidationResult + Send + Sync>;
pub struct CustomSchema {
pub name: String,
pub allowed_types: Vec<ScalarType>,
pub custom_validators: std::collections::HashMap<ScalarType, CustomValidator>,
pub allow_coercion: bool,
}
impl std::fmt::Debug for CustomSchema {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomSchema")
.field("name", &self.name)
.field("allowed_types", &self.allowed_types)
.field("allow_coercion", &self.allow_coercion)
.field(
"validators",
&format!("<{} validators>", self.custom_validators.len()),
)
.finish()
}
}
impl CustomSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
allowed_types: Vec::new(),
custom_validators: std::collections::HashMap::new(),
allow_coercion: true,
}
}
pub fn allow_type(mut self, scalar_type: ScalarType) -> Self {
if !self.allowed_types.contains(&scalar_type) {
self.allowed_types.push(scalar_type);
}
self
}
pub fn allow_types(mut self, types: &[ScalarType]) -> Self {
for &scalar_type in types {
if !self.allowed_types.contains(&scalar_type) {
self.allowed_types.push(scalar_type);
}
}
self
}
pub fn with_validator<F>(mut self, scalar_type: ScalarType, validator: F) -> Self
where
F: Fn(&str, &str) -> CustomValidationResult + Send + Sync + 'static,
{
self.custom_validators
.insert(scalar_type, Box::new(validator));
self
}
pub fn strict(mut self) -> Self {
self.allow_coercion = false;
self
}
pub fn allows_type(&self, scalar_type: ScalarType) -> bool {
self.allowed_types.contains(&scalar_type)
}
pub fn validate_scalar(&self, content: &str, path: &str) -> Result<(), ValidationError> {
let scalar_value = ScalarValue::parse(content.trim());
let scalar_type = scalar_value.scalar_type();
if !self.allows_type(scalar_type) {
if self.allow_coercion {
let mut coerced = false;
for &allowed_type in &self.allowed_types {
if scalar_value.coerce_to_type(allowed_type).is_some() {
coerced = true;
break;
}
}
if !coerced {
return Err(ValidationError::coercion_failed(
path,
&self.name,
scalar_type,
self.allowed_types.clone(),
));
}
} else {
return Err(ValidationError::type_not_allowed(
path,
&self.name,
scalar_type,
self.allowed_types.clone(),
));
}
}
if let Some(validator) = self.custom_validators.get(&scalar_type) {
let result = validator(content.trim(), path);
if let CustomValidationResult::Invalid { constraint, reason } = result {
return Err(ValidationError::custom_constraint_failed(
path,
&self.name,
format!("{}: {}", constraint, reason),
content.trim(),
));
}
}
Ok(())
}
}
#[derive(Debug)]
pub enum Schema {
Failsafe,
Json,
Core,
Custom(CustomSchema),
}
impl PartialEq for Schema {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Schema::Failsafe, Schema::Failsafe) => true,
(Schema::Json, Schema::Json) => true,
(Schema::Core, Schema::Core) => true,
(Schema::Custom(a), Schema::Custom(b)) => a.name == b.name,
_ => false,
}
}
}
impl Schema {
pub fn name(&self) -> &str {
match self {
Schema::Failsafe => "failsafe",
Schema::Json => "json",
Schema::Core => "core",
Schema::Custom(custom) => &custom.name,
}
}
pub fn allows_scalar_type(&self, scalar_type: ScalarType) -> bool {
match self {
Schema::Failsafe => matches!(scalar_type, ScalarType::String),
Schema::Json => matches!(
scalar_type,
ScalarType::String
| ScalarType::Integer
| ScalarType::Float
| ScalarType::Boolean
| ScalarType::Null
),
Schema::Core => true, Schema::Custom(custom) => custom.allows_type(scalar_type),
}
}
pub fn allowed_scalar_types(&self) -> Vec<ScalarType> {
match self {
Schema::Failsafe => vec![ScalarType::String],
Schema::Json => vec![
ScalarType::String,
ScalarType::Integer,
ScalarType::Float,
ScalarType::Boolean,
ScalarType::Null,
],
Schema::Core => vec![
ScalarType::String,
ScalarType::Integer,
ScalarType::Float,
ScalarType::Boolean,
ScalarType::Null,
#[cfg(feature = "base64")]
ScalarType::Binary,
ScalarType::Timestamp,
ScalarType::Regex,
],
Schema::Custom(custom) => custom.allowed_types.clone(),
}
}
}
#[derive(Debug)]
pub struct SchemaValidator {
schema: Schema,
strict: bool,
}
impl SchemaValidator {
pub fn new(schema: Schema) -> Self {
Self {
schema,
strict: false,
}
}
pub fn failsafe() -> Self {
Self::new(Schema::Failsafe)
}
pub fn json() -> Self {
Self::new(Schema::Json)
}
pub fn core() -> Self {
Self::new(Schema::Core)
}
pub fn custom(schema: CustomSchema) -> Self {
Self::new(Schema::Custom(schema))
}
pub fn strict(mut self) -> Self {
self.strict = true;
self
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn validate(&self, document: &Document) -> ValidationResult<()> {
let mut errors = Vec::new();
self.validate_document(document, "root", &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_document(
&self,
document: &Document,
path: &str,
errors: &mut Vec<ValidationError>,
) {
if let Some(scalar) = document.as_scalar() {
self.validate_scalar(&scalar, path, errors);
} else if let Some(sequence) = document.as_sequence() {
self.validate_sequence(&sequence, path, errors);
} else if let Some(mapping) = document.as_mapping() {
self.validate_mapping(&mapping, path, errors);
}
}
fn validate_scalar(&self, scalar: &Scalar, path: &str, errors: &mut Vec<ValidationError>) {
let content = scalar.as_string();
if let Schema::Custom(custom_schema) = &self.schema {
if let Err(error) = custom_schema.validate_scalar(&content, path) {
errors.push(error);
}
return;
}
let scalar_value = ScalarValue::parse(content.trim());
let scalar_type = scalar_value.scalar_type();
if !self.schema.allows_scalar_type(scalar_type) {
if !self.strict {
let allowed_types = self.schema.allowed_scalar_types();
let mut coercion_successful = false;
for allowed_type in allowed_types {
if scalar_value.coerce_to_type(allowed_type).is_some() {
coercion_successful = true;
break;
}
}
if !coercion_successful {
errors.push(ValidationError::coercion_failed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
} else {
errors.push(ValidationError::type_not_allowed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
}
}
fn validate_sequence(&self, seq: &Sequence, path: &str, errors: &mut Vec<ValidationError>) {
for (i, item) in seq.items().enumerate() {
let item_path = format!("{}[{}]", path, i);
self.validate_node(&item, &item_path, errors);
}
}
fn validate_mapping(&self, map: &Mapping, path: &str, errors: &mut Vec<ValidationError>) {
for (key_node, value_node) in map.pairs() {
let key_name = key_node.text().to_string().trim().to_string();
let value_path = format!("{}.{}", path, key_name);
self.validate_node(&value_node, &value_path, errors);
}
}
fn validate_node(
&self,
node: &rowan::SyntaxNode<crate::yaml::Lang>,
path: &str,
errors: &mut Vec<ValidationError>,
) {
use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
if let Some(scalar) = extract_scalar(node) {
self.validate_scalar(&scalar, path, errors);
} else if let Some(tagged_node) = extract_tagged_node(node) {
self.validate_tagged_node(&tagged_node, path, errors);
} else if let Some(sequence) = extract_sequence(node) {
self.validate_sequence(&sequence, path, errors);
} else if let Some(mapping) = extract_mapping(node) {
self.validate_mapping(&mapping, path, errors);
}
}
fn validate_tagged_node(
&self,
tagged_node: &TaggedNode,
path: &str,
errors: &mut Vec<ValidationError>,
) {
if let Schema::Custom(custom_schema) = &self.schema {
let content = tagged_node.to_string();
if let Err(error) = custom_schema.validate_scalar(&content, path) {
errors.push(error);
}
return;
}
let scalar_type = self.get_tagged_node_type(tagged_node);
if !self.schema.allows_scalar_type(scalar_type) {
errors.push(ValidationError::type_not_allowed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
}
fn get_tagged_node_type(&self, tagged_node: &TaggedNode) -> ScalarType {
match tagged_node.tag().as_deref() {
Some("!!timestamp") => ScalarType::Timestamp,
Some("!!regex") => ScalarType::Regex,
Some("!!binary") => {
#[cfg(feature = "base64")]
return ScalarType::Binary;
#[cfg(not(feature = "base64"))]
return ScalarType::String;
}
_ => ScalarType::String,
}
}
pub fn can_coerce(&self, document: &Document) -> ValidationResult<()> {
if self.strict {
return self.validate(document);
}
let mut errors = Vec::new();
self.check_coercion(document, "root", &mut errors);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn check_coercion(&self, document: &Document, path: &str, errors: &mut Vec<ValidationError>) {
if let Some(scalar) = document.as_scalar() {
let scalar_value = ScalarValue::parse(scalar.as_string().trim());
let scalar_type = scalar_value.scalar_type();
if !self.schema.allows_scalar_type(scalar_type) {
let allowed_types = self.schema.allowed_scalar_types();
let mut coerced = false;
for allowed_type in allowed_types {
if scalar_value.coerce_to_type(allowed_type).is_some() {
coerced = true;
break;
}
}
if !coerced {
errors.push(ValidationError::coercion_failed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
}
} else if let Some(sequence) = document.as_sequence() {
for (i, item) in sequence.items().enumerate() {
let item_path = format!("{}[{}]", path, i);
self.check_coercion_node(&item, &item_path, errors);
}
} else if let Some(mapping) = document.as_mapping() {
for (key_node, value_node) in mapping.pairs() {
let key_name = key_node.text().to_string().trim().to_string();
let value_path = format!("{}.{}", path, key_name);
self.check_coercion_node(&value_node, &value_path, errors);
}
}
}
fn check_coercion_node(
&self,
node: &rowan::SyntaxNode<crate::yaml::Lang>,
path: &str,
errors: &mut Vec<ValidationError>,
) {
use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
if let Some(scalar) = extract_scalar(node) {
let scalar_value = ScalarValue::parse(scalar.as_string().trim());
let scalar_type = scalar_value.scalar_type();
if !self.schema.allows_scalar_type(scalar_type) {
let allowed_types = self.schema.allowed_scalar_types();
let mut coerced = false;
for allowed_type in allowed_types {
if scalar_value.coerce_to_type(allowed_type).is_some() {
coerced = true;
break;
}
}
if !coerced {
errors.push(ValidationError::coercion_failed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
}
} else if let Some(tagged_node) = extract_tagged_node(node) {
let scalar_type = self.get_tagged_node_type(&tagged_node);
if !self.schema.allows_scalar_type(scalar_type) {
errors.push(ValidationError::type_not_allowed(
path,
self.schema.name(),
scalar_type,
self.schema.allowed_scalar_types(),
));
}
} else if let Some(sequence) = extract_sequence(node) {
for (i, item) in sequence.items().enumerate() {
let item_path = format!("{}[{}]", path, i);
self.check_coercion_node(&item, &item_path, errors);
}
} else if let Some(mapping) = extract_mapping(node) {
for (key_node, value_node) in mapping.pairs() {
let key_name = key_node.text().to_string().trim().to_string();
let value_path = format!("{}.{}", path, key_name);
self.check_coercion_node(&value_node, &value_path, errors);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::yaml::Document;
use rowan::ast::AstNode;
#[test]
fn test_schema_names() {
assert_eq!(Schema::Failsafe.name(), "failsafe");
assert_eq!(Schema::Json.name(), "json");
assert_eq!(Schema::Core.name(), "core");
}
#[test]
fn test_failsafe_schema_allows_only_strings() {
let schema = Schema::Failsafe;
assert!(schema.allows_scalar_type(ScalarType::String));
assert!(!schema.allows_scalar_type(ScalarType::Integer));
assert!(!schema.allows_scalar_type(ScalarType::Float));
assert!(!schema.allows_scalar_type(ScalarType::Boolean));
assert!(!schema.allows_scalar_type(ScalarType::Null));
#[cfg(feature = "base64")]
assert!(!schema.allows_scalar_type(ScalarType::Binary));
assert!(!schema.allows_scalar_type(ScalarType::Timestamp));
assert!(!schema.allows_scalar_type(ScalarType::Regex));
}
#[test]
fn test_json_schema_allows_json_types() {
let schema = Schema::Json;
assert!(schema.allows_scalar_type(ScalarType::String));
assert!(schema.allows_scalar_type(ScalarType::Integer));
assert!(schema.allows_scalar_type(ScalarType::Float));
assert!(schema.allows_scalar_type(ScalarType::Boolean));
assert!(schema.allows_scalar_type(ScalarType::Null));
#[cfg(feature = "base64")]
assert!(!schema.allows_scalar_type(ScalarType::Binary));
assert!(!schema.allows_scalar_type(ScalarType::Timestamp));
assert!(!schema.allows_scalar_type(ScalarType::Regex));
}
#[test]
fn test_core_schema_allows_all_types() {
let schema = Schema::Core;
assert!(schema.allows_scalar_type(ScalarType::String));
assert!(schema.allows_scalar_type(ScalarType::Integer));
assert!(schema.allows_scalar_type(ScalarType::Float));
assert!(schema.allows_scalar_type(ScalarType::Boolean));
assert!(schema.allows_scalar_type(ScalarType::Null));
#[cfg(feature = "base64")]
assert!(schema.allows_scalar_type(ScalarType::Binary));
assert!(schema.allows_scalar_type(ScalarType::Timestamp));
assert!(schema.allows_scalar_type(ScalarType::Regex));
}
#[test]
fn test_validator_creation() {
let failsafe = SchemaValidator::failsafe();
assert_eq!(*failsafe.schema(), Schema::Failsafe);
assert!(!failsafe.strict);
let json = SchemaValidator::json();
assert_eq!(*json.schema(), Schema::Json);
let core = SchemaValidator::core();
assert_eq!(*core.schema(), Schema::Core);
let strict_validator = SchemaValidator::json().strict();
assert!(strict_validator.strict);
}
#[test]
fn test_validation_error_display() {
let error = ValidationError::type_not_allowed(
"root.items[0]",
"test-schema",
ScalarType::Integer,
vec![ScalarType::String],
);
assert_eq!(
format!("{}", error),
"Validation error at root.items[0]: type Integer not allowed in test-schema schema, expected one of [String]"
);
}
fn create_test_document(content: &str) -> Document {
use crate::yaml::YamlFile;
let parsed = content
.parse::<YamlFile>()
.expect("Failed to parse test YAML");
parsed.document().expect("Expected a document")
}
#[test]
fn test_failsafe_validation_success() {
let yaml_str = r#"
name: "John Doe"
items:
- "item1"
- "item2"
nested:
key: "value"
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::failsafe();
assert!(validator.validate(&document).is_ok());
}
#[test]
fn test_json_validation_success() {
let yaml_str = r#"
name: "John Doe"
age: 30
height: 5.9
active: true
metadata: null
items:
- "item1"
- 42
- true
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::json();
assert!(validator.validate(&document).is_ok());
}
#[test]
fn test_core_validation_success() {
let yaml_str = r#"
name: "John Doe"
age: 30
birth_date: !!timestamp "2001-12-15T02:59:43.1Z"
pattern: !!regex '\d{3}-\d{4}'
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::core();
assert!(validator.validate(&document).is_ok());
}
#[test]
fn test_failsafe_validation_failure() {
let yaml_str = r#"
name: "John"
age: 30
active: true
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::failsafe().strict();
let result = validator.validate(&document);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert!(errors.iter().all(|e| e.schema_name == "failsafe"));
assert!(errors
.iter()
.all(|e| matches!(&e.kind, ValidationErrorKind::TypeNotAllowed { .. })));
}
#[test]
fn test_json_validation_with_yaml_specific_types() {
let yaml_str = r#"
timestamp: !!timestamp "2023-12-25T10:30:45Z"
pattern: !!regex '\d+'
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::json().strict();
let result = validator.validate(&document);
if result.is_ok() {
println!("JSON validation unexpectedly passed!");
println!("Document is mapping: {}", document.as_mapping().is_some());
println!("Document is sequence: {}", document.as_sequence().is_some());
println!("Document is scalar: {}", document.as_scalar().is_some());
if let Some(mapping) = document.as_mapping() {
println!("Mapping has {} pairs", mapping.pairs().count());
for (key, value) in mapping.pairs() {
if let Some(scalar) = Scalar::cast(value.clone()) {
let scalar_value = ScalarValue::parse(scalar.as_string().trim());
println!(
"JSON test - Key '{}' -> Value: '{}' -> Type: {:?}",
key.text().to_string().trim(),
scalar.as_string().trim(),
scalar_value.scalar_type()
);
} else {
println!(
"Value for key '{}' is not a scalar. Node kind: {:?}",
key.text().to_string().trim(),
value.kind()
);
}
}
} else {
println!("Document is not a mapping");
}
} else {
println!("JSON validation correctly failed");
}
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert!(errors.iter().all(|e| e.schema_name == "json"));
assert!(errors
.iter()
.all(|e| matches!(&e.kind, ValidationErrorKind::TypeNotAllowed { .. })));
}
#[test]
fn test_strict_mode_validation() {
let yaml_str = r#"
count: 42
active: true
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::failsafe();
assert!(validator.can_coerce(&document).is_ok());
let strict_validator = SchemaValidator::failsafe().strict();
let result = strict_validator.validate(&document);
assert!(result.is_err());
let string_yaml = r#"
name: hello
message: world
"#;
let string_document = create_test_document(string_yaml);
let non_strict_result = validator.validate(&string_document);
let strict_result = strict_validator.validate(&string_document);
if strict_result.is_err() {
println!("String validation failed!");
if let Some(mapping) = string_document.as_mapping() {
for (key, value) in mapping.pairs() {
if let Some(scalar) = Scalar::cast(value.clone()) {
let scalar_value = ScalarValue::parse(scalar.as_string().trim());
println!(
"String - Key '{}' -> Value: '{}' -> Type: {:?}",
key.text().to_string().trim(),
scalar.as_string().trim(),
scalar_value.scalar_type()
);
}
}
}
if let Err(ref errors) = strict_result {
for error in errors {
println!(" - {}: {}", error.path, error.message());
}
}
}
assert!(non_strict_result.is_ok());
assert!(strict_result.is_ok());
}
#[test]
fn test_validation_error_paths() {
let yaml_str = r#"
users:
- name: "John"
age: 30
- name: "Jane"
active: true
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::failsafe();
let result = validator.validate(&document);
if let Err(errors) = result {
for error in &errors {
assert!(!error.path.is_empty());
assert!(
error.path.starts_with("root"),
"expected path to start with 'root', got: {:?}",
error.path
);
}
}
}
#[test]
fn test_schema_type_lists() {
assert_eq!(
Schema::Failsafe.allowed_scalar_types(),
vec![ScalarType::String]
);
assert_eq!(
Schema::Json.allowed_scalar_types(),
vec![
ScalarType::String,
ScalarType::Integer,
ScalarType::Float,
ScalarType::Boolean,
ScalarType::Null,
]
);
let core_types = Schema::Core.allowed_scalar_types();
let mut expected_core = vec![
ScalarType::String,
ScalarType::Integer,
ScalarType::Float,
ScalarType::Boolean,
ScalarType::Null,
ScalarType::Timestamp,
ScalarType::Regex,
];
#[cfg(feature = "base64")]
expected_core.insert(5, ScalarType::Binary);
assert_eq!(core_types, expected_core);
}
#[test]
fn test_deep_sequence_validation() {
let yaml_str = r#"
numbers:
- 1
- 2.5
- "three"
- true
"#;
let document = create_test_document(yaml_str);
let failsafe_validator = SchemaValidator::failsafe().strict();
let result = failsafe_validator.validate(&document);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
let json_validator = SchemaValidator::json();
let result = json_validator.validate(&document);
assert!(result.is_ok());
let core_validator = SchemaValidator::core();
let result = core_validator.validate(&document);
assert!(result.is_ok());
}
#[test]
fn test_deep_nested_mapping_validation() {
let yaml_str = r#"
user:
name: "John"
details:
age: 30
active: true
scores:
- 95
- 87.5
"#;
let document = create_test_document(yaml_str);
let failsafe_validator = SchemaValidator::failsafe().strict();
let result = failsafe_validator.validate(&document);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
let paths: Vec<&str> = errors.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"root.user.details.age"));
assert!(paths.contains(&"root.user.details.scores[0]"));
let json_validator = SchemaValidator::json();
let result = json_validator.validate(&document);
assert!(result.is_ok());
}
#[test]
fn test_complex_yaml_types_validation() {
let yaml_str = r#"
metadata:
created: !!timestamp "2023-12-25T10:30:45Z"
pattern: !!regex '\d{3}-\d{4}'
values:
- !!timestamp "2023-01-01"
- !!regex '[a-zA-Z]+'
"#;
let document = create_test_document(yaml_str);
let failsafe_validator = SchemaValidator::failsafe().strict();
let result = failsafe_validator.validate(&document);
assert!(result.is_err());
let json_validator = SchemaValidator::json().strict();
let result = json_validator.validate(&document);
assert!(result.is_err());
let core_validator = SchemaValidator::core();
let result = core_validator.validate(&document);
assert!(result.is_ok());
}
#[test]
fn test_coercion_deep_validation() {
let yaml_str = r#"
config:
timeout: "30" # string that looks like number
enabled: "true" # string that looks like boolean
items:
- "42"
- "false"
"#;
let document = create_test_document(yaml_str);
let json_validator = SchemaValidator::json();
let result = json_validator.can_coerce(&document);
assert!(result.is_ok());
let strict_json_validator = SchemaValidator::json().strict();
let result = strict_json_validator.validate(&document);
assert!(result.is_ok());
let problematic_yaml = r#"
data:
timestamp: !!timestamp "2023-12-25"
"#;
let problematic_doc = create_test_document(problematic_yaml);
let result = strict_json_validator.validate(&problematic_doc);
assert!(result.is_err());
}
#[test]
fn test_validation_error_paths_nested() {
let yaml_str = r#"
users:
- name: "Alice"
metadata:
created: !!timestamp "2023-01-01"
tags:
- "admin"
- 42 # This should fail in failsafe
- name: "Bob"
active: true # This should fail in failsafe
"#;
let document = create_test_document(yaml_str);
let validator = SchemaValidator::failsafe().strict();
let result = validator.validate(&document);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
let paths: Vec<&str> = errors.iter().map(|e| e.path.as_str()).collect();
assert!(paths.contains(&"root.users[0].metadata.created"));
assert!(paths.contains(&"root.users[0].metadata.tags[1]"));
assert!(paths.contains(&"root.users[1].active"));
for error in &errors {
println!("Error at {}: {}", error.path, error.message());
}
}
#[test]
fn test_yaml_1_2_spec_compliance() {
let failsafe = Schema::Failsafe;
assert!(failsafe.allows_scalar_type(ScalarType::String));
assert!(!failsafe.allows_scalar_type(ScalarType::Integer));
assert!(!failsafe.allows_scalar_type(ScalarType::Float));
assert!(!failsafe.allows_scalar_type(ScalarType::Boolean));
assert!(!failsafe.allows_scalar_type(ScalarType::Null));
assert!(!failsafe.allows_scalar_type(ScalarType::Timestamp));
let json = Schema::Json;
assert!(json.allows_scalar_type(ScalarType::String));
assert!(json.allows_scalar_type(ScalarType::Integer));
assert!(json.allows_scalar_type(ScalarType::Float));
assert!(json.allows_scalar_type(ScalarType::Boolean));
assert!(json.allows_scalar_type(ScalarType::Null));
assert!(!json.allows_scalar_type(ScalarType::Timestamp)); assert!(!json.allows_scalar_type(ScalarType::Regex)); #[cfg(feature = "base64")]
assert!(!json.allows_scalar_type(ScalarType::Binary));
let core = Schema::Core;
assert!(core.allows_scalar_type(ScalarType::String));
assert!(core.allows_scalar_type(ScalarType::Integer));
assert!(core.allows_scalar_type(ScalarType::Float));
assert!(core.allows_scalar_type(ScalarType::Boolean));
assert!(core.allows_scalar_type(ScalarType::Null));
assert!(core.allows_scalar_type(ScalarType::Timestamp));
assert!(core.allows_scalar_type(ScalarType::Regex));
#[cfg(feature = "base64")]
assert!(core.allows_scalar_type(ScalarType::Binary));
assert_eq!(failsafe.name(), "failsafe");
assert_eq!(json.name(), "json");
assert_eq!(core.name(), "core");
}
#[test]
fn test_spec_compliant_validation_examples() {
let failsafe_yaml = r#"
string: hello
number_as_string: "123"
"#;
let failsafe_doc = create_test_document(failsafe_yaml);
let failsafe_validator = SchemaValidator::failsafe();
assert!(failsafe_validator.validate(&failsafe_doc).is_ok());
let json_yaml = r#"
string: "hello"
number: 42
float: 3.14
boolean: true
null_value: null
"#;
let json_doc = create_test_document(json_yaml);
let json_validator = SchemaValidator::json();
assert!(json_validator.validate(&json_doc).is_ok());
let core_yaml = r#"
timestamp: 2023-01-01T00:00:00Z
regex: !!regex '[0-9]+'
binary: !!binary "SGVsbG8gV29ybGQ="
"#;
let core_doc = create_test_document(core_yaml);
let core_validator = SchemaValidator::core();
assert!(core_validator.validate(&core_doc).is_ok());
let json_strict = SchemaValidator::json().strict();
assert!(json_strict.validate(&core_doc).is_err());
}
#[test]
fn test_custom_schema_basic() {
let custom_schema = CustomSchema::new("test")
.allow_types(&[ScalarType::String, ScalarType::Integer])
.strict();
let validator = SchemaValidator::custom(custom_schema);
let valid_yaml = r#"
name: hello world
count: 42
"#;
let valid_doc = create_test_document(valid_yaml);
let result = validator.validate(&valid_doc);
if let Err(ref errors) = result {
for error in errors {
println!("Valid test error: {}", error);
}
}
assert!(result.is_ok());
let invalid_yaml = r#"
name: hello world
enabled: true # boolean not allowed
"#;
let invalid_doc = create_test_document(invalid_yaml);
let result = validator.validate(&invalid_doc);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].message(),
"type Boolean not allowed in test schema, expected one of [String, Integer]"
);
}
#[test]
fn test_custom_schema_with_validators() {
let custom_schema = CustomSchema::new("email-validation")
.allow_type(ScalarType::String)
.with_validator(ScalarType::String, |value, _path| {
if value.contains('@') && value.contains('.') {
CustomValidationResult::Valid
} else {
CustomValidationResult::invalid("email_format", "invalid email format")
}
});
let validator = SchemaValidator::custom(custom_schema);
let valid_yaml = r#"
email: "user@example.com"
"#;
let valid_doc = create_test_document(valid_yaml);
assert!(validator.validate(&valid_doc).is_ok());
let invalid_yaml = r#"
email: "not-an-email"
"#;
let invalid_doc = create_test_document(invalid_yaml);
let result = validator.validate(&invalid_doc);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].message(),
"custom constraint 'email_format: invalid email format' failed for value 'not-an-email'"
);
}
#[test]
fn test_custom_schema_integer_range() {
let custom_schema = CustomSchema::new("port-validation")
.allow_type(ScalarType::Integer)
.with_validator(ScalarType::Integer, |value, _path| {
if let Ok(port) = value.parse::<u16>() {
if (1024..=65535).contains(&port) {
CustomValidationResult::Valid
} else {
CustomValidationResult::invalid(
"port_range",
format!("port {} must be between 1024 and 65535", port),
)
}
} else {
CustomValidationResult::invalid(
"integer_format",
format!("invalid integer: {}", value),
)
}
});
let validator = SchemaValidator::custom(custom_schema);
let valid_yaml = r#"
port: 8080
"#;
let valid_doc = create_test_document(valid_yaml);
assert!(validator.validate(&valid_doc).is_ok());
let invalid_yaml = r#"
port: 80
"#;
let invalid_doc = create_test_document(invalid_yaml);
let result = validator.validate(&invalid_doc);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert_eq!(
errors[0].message(),
"custom constraint 'port_range: port 80 must be between 1024 and 65535' failed for value '80'"
);
}
#[test]
fn test_custom_schema_multiple_validators() {
let custom_schema = CustomSchema::new("config-validation")
.allow_types(&[ScalarType::String, ScalarType::Integer])
.with_validator(ScalarType::String, |value, _path| {
if value.len() >= 3 {
CustomValidationResult::Valid
} else {
CustomValidationResult::invalid(
"string_length",
format!("string too short: '{}'", value),
)
}
})
.with_validator(ScalarType::Integer, |value, _path| {
if let Ok(num) = value.parse::<i32>() {
if num >= 0 {
CustomValidationResult::Valid
} else {
CustomValidationResult::invalid(
"negative_number",
format!("negative numbers not allowed: {}", num),
)
}
} else {
CustomValidationResult::invalid(
"integer_format",
format!("invalid integer: {}", value),
)
}
});
let validator = SchemaValidator::custom(custom_schema);
let valid_yaml = r#"
name: "valid-name"
count: 100
"#;
let valid_doc = create_test_document(valid_yaml);
assert!(validator.validate(&valid_doc).is_ok());
let invalid_yaml = r#"
name: "ab"
count: 100
"#;
let invalid_doc = create_test_document(invalid_yaml);
let result = validator.validate(&invalid_doc);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].message(),
"custom constraint 'string_length: string too short: 'ab'' failed for value 'ab'"
);
}
#[test]
fn test_custom_schema_strict_mode() {
let custom_schema = CustomSchema::new("strict-test")
.allow_type(ScalarType::String)
.strict();
let validator = SchemaValidator::custom(custom_schema);
let yaml_with_int = r#"
value: 42
"#;
let doc = create_test_document(yaml_with_int);
let result = validator.validate(&doc);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].message(),
"type Integer not allowed in strict-test schema, expected one of [String]"
);
}
#[test]
fn test_custom_schema_name() {
let custom_schema = CustomSchema::new("my-custom-schema").allow_type(ScalarType::String);
let schema = Schema::Custom(custom_schema);
assert_eq!(schema.name(), "my-custom-schema");
}
}