use alloc::boxed::Box;
use alloc::vec::Vec;
use serde_json::Value;
use crate::error::{ErrorIterator, ValidationError, ValidationErrorKind};
use crate::node::SchemaNode;
use crate::paths::LazyLocation;
use foundation_errstacks::IntoErrorTrace;
use super::{Validate, ValidationContext};
pub enum AdditionalItemsPolicy {
AllowAny,
RejectAll,
Validate(SchemaNode),
}
pub struct TupleItemsValidator {
schemas: Vec<SchemaNode>,
additional: AdditionalItemsPolicy,
}
impl TupleItemsValidator {
#[must_use]
pub fn new(schemas: Vec<SchemaNode>, additional: AdditionalItemsPolicy) -> Self {
Self {
schemas,
additional,
}
}
}
impl Validate for TupleItemsValidator {
fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool {
if let Value::Array(arr) = instance {
for (i, item) in arr.iter().enumerate() {
let schema = if i < self.schemas.len() {
&self.schemas[i]
} else {
match &self.additional {
AdditionalItemsPolicy::AllowAny => return true,
AdditionalItemsPolicy::RejectAll => return false,
AdditionalItemsPolicy::Validate(s) => s,
}
};
if !schema.is_valid(item, ctx) {
return false;
}
ctx.mark_item_evaluated(i);
}
}
true
}
fn validate(
&self,
instance: &Value,
instance_path: &LazyLocation<'_>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError> {
if let Value::Array(arr) = instance {
for (i, item) in arr.iter().enumerate() {
let child_path = instance_path.push_index(i);
let schema = if i < self.schemas.len() {
&self.schemas[i]
} else {
match &self.additional {
AdditionalItemsPolicy::AllowAny => continue,
AdditionalItemsPolicy::RejectAll => {
return Err(ValidationErrorKind::AdditionalItems {
limit: self.schemas.len(),
}
.into_error_trace());
}
AdditionalItemsPolicy::Validate(s) => s,
}
};
schema.validate(item, &child_path, ctx)?;
ctx.mark_item_evaluated(i);
}
}
Ok(())
}
fn iter_errors(
&self,
instance: &Value,
instance_path: &LazyLocation<'_>,
ctx: &mut ValidationContext,
) -> ErrorIterator {
let mut errors: Vec<ValidationError> = Vec::new();
if let Value::Array(arr) = instance {
for (i, item) in arr.iter().enumerate() {
let child_path = instance_path.push_index(i);
let schema = if i < self.schemas.len() {
&self.schemas[i]
} else {
match &self.additional {
AdditionalItemsPolicy::AllowAny => continue,
AdditionalItemsPolicy::RejectAll => {
errors.push(
ValidationErrorKind::AdditionalItems {
limit: self.schemas.len(),
}
.into_error_trace(),
);
continue;
}
AdditionalItemsPolicy::Validate(s) => s,
}
};
for e in schema.iter_errors(item, &child_path, ctx) {
errors.push(e);
}
ctx.mark_item_evaluated(i);
}
}
Box::new(errors.into_iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::SchemaNode;
use crate::paths::{LazyLocation, Location};
use serde_json::json;
fn ctx() -> ValidationContext {
ValidationContext::new()
}
#[test]
fn tuple_exact_length_valid() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid],
AdditionalItemsPolicy::RejectAll,
);
assert!(v.is_valid(&json!([1]), &mut ctx()));
}
#[test]
fn tuple_exact_length_too_many_items() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid],
AdditionalItemsPolicy::RejectAll,
);
assert!(!v.is_valid(&json!([1, 2]), &mut ctx()));
}
#[test]
fn tuple_fewer_items_ok() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid, SchemaNode::AlwaysValid],
AdditionalItemsPolicy::RejectAll,
);
assert!(v.is_valid(&json!([1]), &mut ctx()));
}
#[test]
fn tuple_additional_items_allow_any() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid],
AdditionalItemsPolicy::AllowAny,
);
assert!(v.is_valid(&json!([1, "anything", true, null]), &mut ctx()));
}
#[test]
fn tuple_additional_items_validate_schema() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid],
AdditionalItemsPolicy::Validate(SchemaNode::AlwaysValid),
);
assert!(v.is_valid(&json!([1, 2, 3]), &mut ctx()));
}
#[test]
fn tuple_empty_with_reject_all() {
let v = TupleItemsValidator::new(vec![], AdditionalItemsPolicy::RejectAll);
assert!(v.is_valid(&json!([]), &mut ctx()));
assert!(!v.is_valid(&json!([1]), &mut ctx()));
}
#[test]
fn tuple_non_array_instance() {
let v = TupleItemsValidator::new(
vec![SchemaNode::AlwaysValid],
AdditionalItemsPolicy::RejectAll,
);
assert!(v.is_valid(&json!("not an array"), &mut ctx()));
assert!(v.is_valid(&json!({"key": "value"}), &mut ctx()));
}
#[test]
fn tuple_collects_errors_from_multiple_positions() {
let v = TupleItemsValidator::new(
vec![
SchemaNode::AlwaysInvalid {
schema_path: Location::new(),
},
SchemaNode::AlwaysInvalid {
schema_path: Location::new(),
},
],
AdditionalItemsPolicy::RejectAll,
);
let errors: Vec<_> = v
.iter_errors(&json!(["a", "b"]), &LazyLocation::new(), &mut ctx())
.collect();
assert_eq!(errors.len(), 2);
}
}