use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use foundation_errstacks::{ErrorTrace, IntoErrorTrace};
use serde_json::Value;
use crate::paths::Location;
use crate::types::{JsonType, JsonTypeSet};
pub type ValidationError = ErrorTrace<ValidationErrorKind>;
pub type ErrorIterator = Box<dyn Iterator<Item = ValidationError>>;
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
pub enum ValidationErrorKind {
InvalidType {
expected: JsonTypeSet,
actual: JsonType,
},
InvalidConst {
expected: Value,
},
InvalidEnum {
options: Vec<Value>,
},
MinLength {
min: u64,
actual: u64,
},
MaxLength {
max: u64,
actual: u64,
},
Pattern {
pattern: String,
},
Format {
format: String,
},
Minimum {
limit: f64,
actual: f64,
exclusive: bool,
},
Maximum {
limit: f64,
actual: f64,
exclusive: bool,
},
MultipleOf {
multiple: f64,
actual: f64,
},
Required {
property: String,
},
MinProperties {
min: u64,
actual: u64,
},
MaxProperties {
max: u64,
actual: u64,
},
AdditionalProperties {
unexpected: Vec<String>,
},
PropertyNames {
invalid_name: String,
},
MinItems {
min: u64,
actual: u64,
},
MaxItems {
max: u64,
actual: u64,
},
UniqueItems {
first: usize,
second: usize,
},
Contains,
MinContains {
min: u64,
actual: u64,
},
MaxContains {
max: u64,
actual: u64,
},
AdditionalItems {
limit: usize,
},
Not,
AnyOf,
OneOfNotValid,
OneOfMultipleValid,
IfThenElse,
RefNotFound {
reference: String,
},
ContentEncoding {
encoding: String,
},
ContentMediaType {
media_type: String,
},
ContentSchema,
FalseSchema,
UnevaluatedProperties {
properties: Vec<String>,
},
UnevaluatedItems {
index: usize,
},
DependentRequired {
property: String,
missing: Vec<String>,
},
Schema {
reason: String,
},
Custom {
message: String,
},
}
#[allow(clippy::too_many_lines)]
impl core::fmt::Display for ValidationErrorKind {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidType { expected, actual } => {
write!(f, "value is not of type(s) {expected}, got {actual}")
}
Self::InvalidConst { expected } => {
write!(f, "value does not match constant {expected}")
}
Self::InvalidEnum { options } => {
write!(f, "value is not one of the allowed options: {options:?}")
}
Self::MinLength { min, actual } => {
write!(f, "string is too short (length {actual}, minimum {min})")
}
Self::MaxLength { max, actual } => {
write!(f, "string is too long (length {actual}, maximum {max})")
}
Self::Pattern { pattern } => write!(f, "string does not match pattern '{pattern}'"),
Self::Format { format } => write!(f, "value does not match format '{format}'"),
Self::Minimum {
limit,
actual,
exclusive: true,
} => write!(f, "value {actual} is less than or equal to minimum {limit}"),
Self::Minimum {
limit,
actual,
exclusive: false,
} => write!(f, "value {actual} is less than minimum {limit}"),
Self::Maximum {
limit,
actual,
exclusive: true,
} => write!(
f,
"value {actual} is greater than or equal to maximum {limit}"
),
Self::Maximum {
limit,
actual,
exclusive: false,
} => write!(f, "value {actual} is greater than maximum {limit}"),
Self::MultipleOf { multiple, actual } => {
write!(f, "value {actual} is not a multiple of {multiple}")
}
Self::Required { property } => write!(f, "'{property}' is a required property"),
Self::MinProperties { min, actual } => write!(
f,
"object has too few properties (count {actual}, minimum {min})"
),
Self::MaxProperties { max, actual } => write!(
f,
"object has too many properties (count {actual}, maximum {max})"
),
Self::AdditionalProperties { unexpected } => write!(
f,
"additional properties are not allowed: {}",
unexpected.join(", ")
),
Self::PropertyNames { invalid_name } => {
write!(f, "property name '{invalid_name}' is invalid")
}
Self::MinItems { min, actual } => {
write!(f, "array has too few items (count {actual}, minimum {min})")
}
Self::MaxItems { max, actual } => write!(
f,
"array has too many items (count {actual}, maximum {max})"
),
Self::UniqueItems { first, second } => write!(
f,
"array items at indices {first} and {second} are not unique"
),
Self::Contains => write!(f, "array does not contain any matching item"),
Self::MinContains { min, actual } => write!(
f,
"array contains too few matching items (count {actual}, minimum {min})"
),
Self::MaxContains { max, actual } => write!(
f,
"array contains too many matching items (count {actual}, maximum {max})"
),
Self::AdditionalItems { limit } => {
write!(f, "array has additional items beyond index {limit}")
}
Self::Not => write!(f, "value matches the 'not' schema"),
Self::AnyOf => write!(f, "value does not match any of the 'anyOf' schemas"),
Self::OneOfNotValid => {
write!(f, "value does not match exactly one of the 'oneOf' schemas")
}
Self::OneOfMultipleValid => {
write!(f, "value matches more than one of the 'oneOf' schemas")
}
Self::IfThenElse => write!(f, "conditional validation failed (if/then/else)"),
Self::RefNotFound { reference } => {
write!(f, "reference '{reference}' could not be resolved")
}
Self::ContentEncoding { encoding } => {
write!(f, "content encoding '{encoding}' is invalid")
}
Self::ContentMediaType { media_type } => {
write!(f, "content media type '{media_type}' is invalid")
}
Self::ContentSchema => {
write!(f, "content does not conform to contentSchema")
}
Self::FalseSchema => write!(f, "false schema always fails validation"),
Self::UnevaluatedProperties { properties } => {
write!(f, "unevaluated properties: {}", properties.join(", "))
}
Self::UnevaluatedItems { index } => write!(f, "unevaluated item at index {index}"),
Self::DependentRequired { property, missing } => {
write!(f, "property '{property}' requires: {}", missing.join(", "))
}
Self::Schema { reason } => write!(f, "schema error: {reason}"),
Self::Custom { message } => write!(f, "{message}"),
}
}
}
impl ValidationErrorKind {
#[must_use]
pub const fn keyword_name(&self) -> Option<&'static str> {
match self {
Self::InvalidType { .. } => Some("type"),
Self::InvalidConst { .. } => Some("const"),
Self::InvalidEnum { .. } => Some("enum"),
Self::MinLength { .. } => Some("minLength"),
Self::MaxLength { .. } => Some("maxLength"),
Self::Pattern { .. } => Some("pattern"),
Self::Format { .. } => Some("format"),
Self::Minimum { .. } => Some("minimum"),
Self::Maximum { .. } => Some("maximum"),
Self::MultipleOf { .. } => Some("multipleOf"),
Self::Required { .. } => Some("required"),
Self::MinProperties { .. } => Some("minProperties"),
Self::MaxProperties { .. } => Some("maxProperties"),
Self::AdditionalProperties { .. } => Some("additionalProperties"),
Self::PropertyNames { .. } => Some("propertyNames"),
Self::MinItems { .. } => Some("minItems"),
Self::MaxItems { .. } => Some("maxItems"),
Self::UniqueItems { .. } => Some("uniqueItems"),
Self::Contains { .. } => Some("contains"),
Self::MinContains { .. } => Some("minContains"),
Self::MaxContains { .. } => Some("maxContains"),
Self::AdditionalItems { .. } => Some("additionalItems"),
Self::Not { .. } => Some("not"),
Self::AnyOf { .. } => Some("anyOf"),
Self::OneOfNotValid { .. } | Self::OneOfMultipleValid { .. } => Some("oneOf"),
Self::IfThenElse { .. } => Some("if"),
Self::RefNotFound { .. } => Some("$ref"),
Self::ContentEncoding { .. } => Some("contentEncoding"),
Self::ContentMediaType { .. } => Some("contentMediaType"),
Self::ContentSchema => Some("contentSchema"),
Self::UnevaluatedProperties { .. } => Some("unevaluatedProperties"),
Self::UnevaluatedItems { .. } => Some("unevaluatedItems"),
Self::DependentRequired { .. } => Some("dependentRequired"),
Self::FalseSchema { .. } | Self::Schema { .. } | Self::Custom { .. } => None,
}
}
}
impl core::error::Error for ValidationErrorKind {}
#[derive(Debug, Clone)]
pub struct ValidationFailure {
pub kind: ValidationErrorKind,
pub instance_path: Location,
pub schema_path: Location,
}
pub struct ValidationErrorBuilder {
instance_path: Location,
schema_path: Location,
}
impl ValidationErrorBuilder {
#[must_use]
pub fn new(instance_path: Location, schema_path: Location) -> Self {
Self {
instance_path,
schema_path,
}
}
#[must_use]
pub fn build(self, kind: ValidationErrorKind) -> ValidationError {
kind.into_error_trace()
.attach_opaque(self.instance_path)
.attach_opaque(self.schema_path)
}
pub fn build_with_message(
self,
kind: ValidationErrorKind,
message: impl Into<String>,
) -> ValidationError {
self.build(kind).attach(message.into())
}
}
#[must_use]
pub fn to_failure(error: &ValidationError) -> Option<ValidationFailure> {
let kind = error.current_context().clone();
let mut locations: Vec<&Location> = error
.frames()
.filter_map(|f| f.as_any().downcast_ref::<Location>())
.collect();
if locations.len() < 2 {
return None;
}
let instance_path = locations.remove(0).clone();
let schema_path = locations.remove(0).clone();
Some(ValidationFailure {
kind,
instance_path,
schema_path,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_error_kind_display_type() {
let kind = ValidationErrorKind::InvalidType {
expected: JsonTypeSet::with(JsonType::Number),
actual: JsonType::String,
};
let msg = format!("{kind}");
assert!(msg.contains("not of type"));
assert!(msg.contains("number"));
assert!(msg.contains("string"));
}
#[test]
fn test_validation_error_kind_display_required() {
let kind = ValidationErrorKind::Required {
property: "name".into(),
};
let msg = format!("{kind}");
assert!(msg.contains("name"));
assert!(msg.contains("required"));
}
#[test]
fn test_validation_error_kind_display_minimum() {
let kind = ValidationErrorKind::Minimum {
limit: 18.0,
actual: 12.0,
exclusive: false,
};
let msg = format!("{kind}");
assert!(msg.contains("12"));
assert!(msg.contains("18"));
assert!(msg.contains("less than"));
}
#[test]
fn test_validation_error_kind_display_false_schema() {
let kind = ValidationErrorKind::FalseSchema;
assert_eq!(format!("{kind}"), "false schema always fails validation");
}
#[test]
fn test_validation_error_kind_display_custom() {
let kind = ValidationErrorKind::Custom {
message: "my custom error".into(),
};
let msg = format!("{kind}");
assert!(msg.contains("my custom error"));
}
#[test]
fn test_keyword_name() {
assert_eq!(
ValidationErrorKind::Required {
property: "x".into()
}
.keyword_name(),
Some("required")
);
assert_eq!(
ValidationErrorKind::InvalidType {
expected: JsonTypeSet::new(),
actual: JsonType::Null
}
.keyword_name(),
Some("type")
);
assert_eq!(ValidationErrorKind::FalseSchema.keyword_name(), None);
assert_eq!(
ValidationErrorKind::Custom {
message: String::new()
}
.keyword_name(),
None
);
}
#[test]
fn test_validation_error_builder() {
let error = ValidationErrorBuilder::new(Location::new(), Location::new()).build(
ValidationErrorKind::Required {
property: "x".into(),
},
);
let failure = to_failure(&error);
assert!(failure.is_some());
let failure = failure.unwrap();
assert_eq!(
failure.kind,
ValidationErrorKind::Required {
property: "x".into()
}
);
}
}