use serde_json::{json, Value};
use std::collections::HashMap;
use stillwater::Validation;
use crate::error::{SchemaError, SchemaErrors};
use crate::interop::ToJsonSchema;
use crate::path::JsonPath;
use super::traits::SchemaLike;
enum ArrayConstraint {
MinLength {
min: usize,
message: Option<String>,
},
MaxLength {
max: usize,
message: Option<String>,
},
Unique {
message: Option<String>,
},
UniqueBy {
key_fn: Box<dyn Fn(&Value) -> Value + Send + Sync>,
message: Option<String>,
},
}
pub struct ArraySchema<S> {
item_schema: S,
constraints: Vec<ArrayConstraint>,
type_error_message: Option<String>,
}
impl<S: SchemaLike> ArraySchema<S> {
pub fn new(item_schema: S) -> Self {
Self {
item_schema,
constraints: Vec::new(),
type_error_message: None,
}
}
pub fn min_len(mut self, min: usize) -> Self {
self.constraints
.push(ArrayConstraint::MinLength { min, message: None });
self
}
pub fn max_len(mut self, max: usize) -> Self {
self.constraints
.push(ArrayConstraint::MaxLength { max, message: None });
self
}
pub fn non_empty(self) -> Self {
self.min_len(1)
}
pub fn unique(mut self) -> Self {
self.constraints
.push(ArrayConstraint::Unique { message: None });
self
}
pub fn unique_by<F>(mut self, key_fn: F) -> Self
where
F: Fn(&Value) -> Value + Send + Sync + 'static,
{
self.constraints.push(ArrayConstraint::UniqueBy {
key_fn: Box::new(key_fn),
message: None,
});
self
}
pub fn error(mut self, message: impl Into<String>) -> Self {
if let Some(last) = self.constraints.last_mut() {
match last {
ArrayConstraint::MinLength { message: m, .. } => *m = Some(message.into()),
ArrayConstraint::MaxLength { message: m, .. } => *m = Some(message.into()),
ArrayConstraint::Unique { message: m } => *m = Some(message.into()),
ArrayConstraint::UniqueBy { message: m, .. } => *m = Some(message.into()),
}
} else {
self.type_error_message = Some(message.into());
}
self
}
pub fn validate(&self, value: &Value, path: &JsonPath) -> Validation<Vec<Value>, SchemaErrors> {
let arr = match value.as_array() {
Some(a) => a,
None => {
let message = self
.type_error_message
.clone()
.unwrap_or_else(|| "expected array".to_string());
return Validation::Failure(SchemaErrors::single(
SchemaError::new(path.clone(), message)
.with_code("invalid_type")
.with_got(value_type_name(value))
.with_expected("array"),
));
}
};
let mut errors = Vec::new();
for constraint in &self.constraints {
match constraint {
ArrayConstraint::MinLength { min, message } if arr.len() < *min => {
let msg = message.clone().unwrap_or_else(|| {
format!("array must have at least {} items, got {}", min, arr.len())
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("min_length")
.with_expected(format!("at least {} items", min))
.with_got(format!("{} items", arr.len())),
);
}
ArrayConstraint::MaxLength { max, message } if arr.len() > *max => {
let msg = message.clone().unwrap_or_else(|| {
format!("array must have at most {} items, got {}", max, arr.len())
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("max_length")
.with_expected(format!("at most {} items", max))
.with_got(format!("{} items", arr.len())),
);
}
_ => {}
}
}
let mut validated_items = Vec::with_capacity(arr.len());
for (index, item) in arr.iter().enumerate() {
let item_path = path.push_index(index);
match self.item_schema.validate_to_value(item, &item_path) {
Validation::Success(v) => validated_items.push(v),
Validation::Failure(e) => errors.extend(e.into_iter()),
}
}
for constraint in &self.constraints {
match constraint {
ArrayConstraint::Unique { message } => {
let duplicates = find_duplicates(arr, |v| v.clone());
for indices in duplicates.values() {
if indices.len() > 1 {
let msg = message.clone().unwrap_or_else(|| {
format!("duplicate value at indices {:?}", indices)
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("unique")
.with_got(format!("duplicates at indices {:?}", indices)),
);
}
}
}
ArrayConstraint::UniqueBy { key_fn, message } => {
let duplicates = find_duplicates(arr, key_fn);
for indices in duplicates.values() {
if indices.len() > 1 {
let msg = message.clone().unwrap_or_else(|| {
format!("duplicate key at indices {:?}", indices)
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("unique")
.with_got(format!("duplicates at indices {:?}", indices)),
);
}
}
}
_ => {}
}
}
if errors.is_empty() {
Validation::Success(validated_items)
} else {
Validation::Failure(SchemaErrors::from_vec(errors))
}
}
}
impl<S: SchemaLike> SchemaLike for ArraySchema<S> {
type Output = Vec<Value>;
fn validate(&self, value: &Value, path: &JsonPath) -> Validation<Self::Output, SchemaErrors> {
self.validate(value, path)
}
fn validate_to_value(&self, value: &Value, path: &JsonPath) -> Validation<Value, SchemaErrors> {
self.validate(value, path).map(Value::Array)
}
fn validate_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Self::Output, SchemaErrors> {
let arr = match value.as_array() {
Some(a) => a,
None => {
let message = self
.type_error_message
.clone()
.unwrap_or_else(|| "expected array".to_string());
return Validation::Failure(SchemaErrors::single(
SchemaError::new(path.clone(), message)
.with_code("invalid_type")
.with_got(value_type_name(value))
.with_expected("array"),
));
}
};
let mut errors = Vec::new();
for constraint in &self.constraints {
match constraint {
ArrayConstraint::MinLength { min, message } if arr.len() < *min => {
let msg = message.clone().unwrap_or_else(|| {
format!("array must have at least {} items, got {}", min, arr.len())
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("min_length")
.with_expected(format!("at least {} items", min))
.with_got(format!("{} items", arr.len())),
);
}
ArrayConstraint::MaxLength { max, message } if arr.len() > *max => {
let msg = message.clone().unwrap_or_else(|| {
format!("array must have at most {} items, got {}", max, arr.len())
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("max_length")
.with_expected(format!("at most {} items", max))
.with_got(format!("{} items", arr.len())),
);
}
_ => {}
}
}
let mut validated_items = Vec::with_capacity(arr.len());
for (index, item) in arr.iter().enumerate() {
let item_path = path.push_index(index);
match self
.item_schema
.validate_to_value_with_context(item, &item_path, context)
{
Validation::Success(v) => validated_items.push(v),
Validation::Failure(e) => errors.extend(e.into_iter()),
}
}
for constraint in &self.constraints {
match constraint {
ArrayConstraint::Unique { message } => {
let duplicates = find_duplicates(arr, |v| v.clone());
for indices in duplicates.values() {
if indices.len() > 1 {
let msg = message.clone().unwrap_or_else(|| {
format!("duplicate value at indices {:?}", indices)
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("unique")
.with_got(format!("duplicates at indices {:?}", indices)),
);
}
}
}
ArrayConstraint::UniqueBy { key_fn, message } => {
let duplicates = find_duplicates(arr, key_fn);
for indices in duplicates.values() {
if indices.len() > 1 {
let msg = message.clone().unwrap_or_else(|| {
format!("duplicate key at indices {:?}", indices)
});
errors.push(
SchemaError::new(path.clone(), msg)
.with_code("unique")
.with_got(format!("duplicates at indices {:?}", indices)),
);
}
}
}
_ => {}
}
}
if errors.is_empty() {
Validation::Success(validated_items)
} else {
Validation::Failure(SchemaErrors::from_vec(errors))
}
}
fn validate_to_value_with_context(
&self,
value: &Value,
path: &JsonPath,
context: &crate::validation::ValidationContext,
) -> Validation<Value, SchemaErrors> {
self.validate_with_context(value, path, context)
.map(Value::Array)
}
fn collect_refs(&self, refs: &mut Vec<String>) {
self.item_schema.collect_refs(refs);
}
}
impl<S: SchemaLike + ToJsonSchema> ToJsonSchema for ArraySchema<S> {
fn to_json_schema(&self) -> Value {
let mut schema = json!({
"type": "array",
"items": self.item_schema.to_json_schema(),
});
for constraint in &self.constraints {
match constraint {
ArrayConstraint::MinLength { min, .. } => {
schema["minItems"] = json!(min);
}
ArrayConstraint::MaxLength { max, .. } => {
schema["maxItems"] = json!(max);
}
ArrayConstraint::Unique { .. } => {
schema["uniqueItems"] = json!(true);
}
_ => {}
}
}
schema
}
}
fn find_duplicates<F>(arr: &[Value], key_fn: F) -> HashMap<String, Vec<usize>>
where
F: Fn(&Value) -> Value,
{
let mut seen: HashMap<String, Vec<usize>> = HashMap::new();
for (i, item) in arr.iter().enumerate() {
let key = key_fn(item);
let key_str = serde_json::to_string(&key).unwrap_or_else(|_| format!("{:?}", key));
seen.entry(key_str).or_default().push(i);
}
seen
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{IntegerSchema, ObjectSchema, StringSchema};
use serde_json::json;
fn unwrap_success<T, E: std::fmt::Debug>(v: Validation<T, E>) -> T {
v.into_result().unwrap()
}
fn unwrap_failure<T: std::fmt::Debug, E>(v: Validation<T, E>) -> E {
v.into_result().unwrap_err()
}
#[test]
fn test_array_schema_accepts_array() {
let schema = ArraySchema::new(StringSchema::new());
let result = schema.validate(&json!(["hello", "world"]), &JsonPath::root());
assert!(result.is_success());
let items = unwrap_success(result);
assert_eq!(items, vec![json!("hello"), json!("world")]);
}
#[test]
fn test_array_schema_accepts_empty_array() {
let schema = ArraySchema::new(StringSchema::new());
let result = schema.validate(&json!([]), &JsonPath::root());
assert!(result.is_success());
let items = unwrap_success(result);
assert!(items.is_empty());
}
#[test]
fn test_array_schema_rejects_non_array() {
let schema = ArraySchema::new(StringSchema::new());
let result = schema.validate(&json!("not an array"), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "invalid_type");
assert_eq!(errors.first().got, Some("string".to_string()));
let result = schema.validate(&json!(42), &JsonPath::root());
assert!(result.is_failure());
let result = schema.validate(&json!(null), &JsonPath::root());
assert!(result.is_failure());
let result = schema.validate(&json!({"key": "value"}), &JsonPath::root());
assert!(result.is_failure());
}
#[test]
fn test_array_validates_items() {
let schema = ArraySchema::new(IntegerSchema::new().positive());
let result = schema.validate(&json!([1, 2, 3]), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_array_reports_invalid_items() {
let schema = ArraySchema::new(IntegerSchema::new().positive());
let result = schema.validate(&json!([1, -2, 3]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().code, "positive");
assert_eq!(errors.first().path.to_string(), "[1]");
}
#[test]
fn test_array_accumulates_multiple_item_errors() {
let schema = ArraySchema::new(IntegerSchema::new().positive());
let result = schema.validate(&json!([-1, -2, 3, -4]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 3);
}
#[test]
fn test_array_validates_nested_objects() {
let user_schema = ObjectSchema::new()
.field("name", StringSchema::new().min_len(1))
.field("age", IntegerSchema::new().positive());
let schema = ArraySchema::new(user_schema);
let result = schema.validate(
&json!([
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]),
&JsonPath::root(),
);
assert!(result.is_success());
let result = schema.validate(
&json!([
{"name": "", "age": 30},
{"name": "Bob", "age": -5}
]),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
let paths: Vec<_> = errors.iter().map(|e| e.path.to_string()).collect();
assert!(paths.contains(&"[0].name".to_string()));
assert!(paths.contains(&"[1].age".to_string()));
}
#[test]
fn test_min_len_constraint() {
let schema = ArraySchema::new(StringSchema::new()).min_len(2);
let result = schema.validate(&json!(["a", "b"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a", "b", "c"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a"]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "min_length");
}
#[test]
fn test_max_len_constraint() {
let schema = ArraySchema::new(StringSchema::new()).max_len(3);
let result = schema.validate(&json!(["a", "b"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a", "b", "c"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a", "b", "c", "d"]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "max_length");
}
#[test]
fn test_non_empty_constraint() {
let schema = ArraySchema::new(StringSchema::new()).non_empty();
let result = schema.validate(&json!(["a"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!([]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "min_length");
}
#[test]
fn test_combined_length_constraints() {
let schema = ArraySchema::new(StringSchema::new()).min_len(2).max_len(4);
let result = schema.validate(&json!(["a"]), &JsonPath::root());
assert!(result.is_failure());
let result = schema.validate(&json!(["a", "b"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a", "b", "c", "d"]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!(["a", "b", "c", "d", "e"]), &JsonPath::root());
assert!(result.is_failure());
}
#[test]
fn test_unique_constraint_with_distinct_values() {
let schema = ArraySchema::new(StringSchema::new()).unique();
let result = schema.validate(&json!(["a", "b", "c"]), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_unique_constraint_with_duplicates() {
let schema = ArraySchema::new(StringSchema::new()).unique();
let result = schema.validate(&json!(["a", "b", "a"]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "unique");
}
#[test]
fn test_unique_constraint_with_integers() {
let schema = ArraySchema::new(IntegerSchema::new()).unique();
let result = schema.validate(&json!([1, 2, 3]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!([1, 2, 1]), &JsonPath::root());
assert!(result.is_failure());
}
#[test]
fn test_unique_constraint_empty_array() {
let schema = ArraySchema::new(StringSchema::new()).unique();
let result = schema.validate(&json!([]), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_unique_constraint_single_item() {
let schema = ArraySchema::new(StringSchema::new()).unique();
let result = schema.validate(&json!(["only"]), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_unique_by_constraint() {
let user_schema = ObjectSchema::new()
.field("id", IntegerSchema::new())
.field("name", StringSchema::new());
let schema = ArraySchema::new(user_schema)
.unique_by(|v| v.get("id").cloned().unwrap_or(Value::Null));
let result = schema.validate(
&json!([
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]),
&JsonPath::root(),
);
assert!(result.is_success());
let result = schema.validate(
&json!([
{"id": 1, "name": "Alice"},
{"id": 1, "name": "Bob"}
]),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().code, "unique");
}
#[test]
fn test_error_accumulation_length_and_items() {
let schema = ArraySchema::new(IntegerSchema::new().positive()).min_len(3);
let result = schema.validate(&json!([-1, -2]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 3);
assert_eq!(errors.with_code("min_length").len(), 1);
assert_eq!(errors.with_code("positive").len(), 2);
}
#[test]
fn test_error_accumulation_all_constraint_types() {
let schema = ArraySchema::new(IntegerSchema::new().positive())
.min_len(5)
.unique();
let result = schema.validate(&json!([1, -2, 1]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 3);
}
#[test]
fn test_path_tracking_simple() {
let schema = ArraySchema::new(StringSchema::new().min_len(5));
let result = schema.validate(&json!(["hi"]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().path.to_string(), "[0]");
}
#[test]
fn test_path_tracking_nested() {
let inner_schema = ObjectSchema::new().field("value", IntegerSchema::new().positive());
let schema = ArraySchema::new(inner_schema);
let path = JsonPath::root().push_field("items");
let result = schema.validate(&json!([{"value": -5}]), &path);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().path.to_string(), "items[0].value");
}
#[test]
fn test_path_tracking_deeply_nested() {
let inner_array = ArraySchema::new(IntegerSchema::new().positive());
let outer_schema = ObjectSchema::new().field("numbers", inner_array);
let outer_array = ArraySchema::new(outer_schema);
let result = outer_array.validate(
&json!([
{"numbers": [1, -2, 3]}
]),
&JsonPath::root(),
);
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().path.to_string(), "[0].numbers[1]");
}
#[test]
fn test_custom_type_error_message() {
let schema = ArraySchema::new(StringSchema::new()).error("must be a list of tags");
let result = schema.validate(&json!("not an array"), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().message, "must be a list of tags");
}
#[test]
fn test_custom_min_length_error_message() {
let schema = ArraySchema::new(StringSchema::new())
.min_len(1)
.error("at least one tag is required");
let result = schema.validate(&json!([]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().message, "at least one tag is required");
}
#[test]
fn test_custom_unique_error_message() {
let schema = ArraySchema::new(StringSchema::new())
.unique()
.error("all tags must be unique");
let result = schema.validate(&json!(["a", "a"]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.first().message, "all tags must be unique");
}
#[test]
fn test_array_of_nulls() {
let schema = ArraySchema::new(StringSchema::new());
let result = schema.validate(&json!([null, null]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_mixed_type_array() {
let schema = ArraySchema::new(IntegerSchema::new());
let result = schema.validate(&json!([1, "two", 3]), &JsonPath::root());
assert!(result.is_failure());
let errors = unwrap_failure(result);
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().path.to_string(), "[1]");
}
#[test]
fn test_large_array() {
let schema = ArraySchema::new(IntegerSchema::new());
let large_array: Vec<i32> = (0..1000).collect();
let result = schema.validate(&json!(large_array), &JsonPath::root());
assert!(result.is_success());
}
#[test]
fn test_unique_with_objects() {
let schema = ArraySchema::new(ObjectSchema::new()).unique();
let result = schema.validate(&json!([{"a": 1}, {"a": 2}]), &JsonPath::root());
assert!(result.is_success());
let result = schema.validate(&json!([{"a": 1}, {"a": 1}]), &JsonPath::root());
assert!(result.is_failure());
}
#[test]
fn test_schema_like_validate_to_value() {
let schema = ArraySchema::new(StringSchema::new());
let result = schema.validate_to_value(&json!(["hello"]), &JsonPath::root());
assert!(result.is_success());
match result.into_result().unwrap() {
Value::Array(arr) => {
assert_eq!(arr, vec![json!("hello")]);
}
_ => panic!("Expected array"),
}
}
}