use rayon::prelude::*;
use serde_json::Value;
use stillwater::Validation;
use crate::error::SchemaErrors;
use crate::path::JsonPath;
use crate::schema::StringSchema;
pub trait AsyncValidator<E>: Send + Sync {
fn validate_async(
&self,
value: &Value,
path: &JsonPath,
env: &E,
) -> Validation<(), SchemaErrors>;
}
pub struct AsyncStringSchema<E> {
sync_schema: StringSchema,
async_validators: Vec<Box<dyn AsyncValidator<E>>>,
}
impl<E> AsyncStringSchema<E> {
pub fn new(sync_schema: StringSchema) -> Self {
Self {
sync_schema,
async_validators: Vec::new(),
}
}
pub fn async_custom<V>(mut self, validator: V) -> Self
where
V: AsyncValidator<E> + 'static,
{
self.async_validators.push(Box::new(validator));
self
}
pub fn validate_with_env(
&self,
value: &Value,
path: &JsonPath,
env: &E,
) -> Validation<String, SchemaErrors> {
let sync_result = self.sync_schema.validate(value, path);
match sync_result {
Validation::Failure(errors) => {
Validation::Failure(errors)
}
Validation::Success(validated) => {
let mut all_errors = Vec::new();
for validator in &self.async_validators {
let result = validator.validate_async(value, path, env);
if let Validation::Failure(errors) = result {
all_errors.extend(errors.into_iter());
}
}
if all_errors.is_empty() {
Validation::Success(validated)
} else {
Validation::Failure(SchemaErrors::from_vec(all_errors))
}
}
}
}
pub fn validate_with_env_parallel(
&self,
value: &Value,
path: &JsonPath,
env: &E,
) -> Validation<String, SchemaErrors>
where
E: Sync,
{
let sync_result = self.sync_schema.validate(value, path);
match sync_result {
Validation::Failure(errors) => {
Validation::Failure(errors)
}
Validation::Success(validated) => {
let all_errors: Vec<_> = self
.async_validators
.par_iter()
.flat_map(|validator| {
let result = validator.validate_async(value, path, env);
match result {
Validation::Failure(errors) => errors.into_iter().collect::<Vec<_>>(),
Validation::Success(_) => Vec::new(),
}
})
.collect();
if all_errors.is_empty() {
Validation::Success(validated)
} else {
Validation::Failure(SchemaErrors::from_vec(all_errors))
}
}
}
}
}
impl StringSchema {
pub fn to_async<E>(self) -> AsyncStringSchema<E> {
AsyncStringSchema::new(self)
}
pub fn async_custom<E, V>(self, validator: V) -> AsyncStringSchema<E>
where
V: AsyncValidator<E> + 'static,
{
AsyncStringSchema::new(self).async_custom(validator)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::SchemaError;
use crate::Schema;
use serde_json::json;
struct TestEnv;
struct AlwaysFailValidator {
message: String,
}
impl AsyncValidator<TestEnv> for AlwaysFailValidator {
fn validate_async(
&self,
_value: &Value,
path: &JsonPath,
_env: &TestEnv,
) -> Validation<(), SchemaErrors> {
Validation::Failure(SchemaErrors::single(SchemaError::new(
path.clone(),
self.message.clone(),
)))
}
}
struct AlwaysPassValidator;
impl AsyncValidator<TestEnv> for AlwaysPassValidator {
fn validate_async(
&self,
_value: &Value,
_path: &JsonPath,
_env: &TestEnv,
) -> Validation<(), SchemaErrors> {
Validation::Success(())
}
}
#[test]
fn test_async_validator_pass() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysPassValidator);
let env = TestEnv;
let result = schema.validate_with_env(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_success());
}
#[test]
fn test_async_validator_fail() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysFailValidator {
message: "async validation failed".to_string(),
});
let env = TestEnv;
let result = schema.validate_with_env(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_failure());
}
#[test]
fn test_sync_fail_skips_async() {
let schema = Schema::string()
.min_len(10)
.async_custom(AlwaysPassValidator);
let env = TestEnv;
let result = schema.validate_with_env(&json!("hi"), &JsonPath::root(), &env);
assert!(result.is_failure());
}
#[test]
fn test_multiple_async_validators() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysFailValidator {
message: "first error".to_string(),
})
.async_custom(AlwaysFailValidator {
message: "second error".to_string(),
});
let env = TestEnv;
let result = schema.validate_with_env(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 2);
}
}
#[test]
fn test_parallel_async_validator_pass() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysPassValidator)
.async_custom(AlwaysPassValidator);
let env = TestEnv;
let result = schema.validate_with_env_parallel(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_success());
}
#[test]
fn test_parallel_async_validator_fail() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysFailValidator {
message: "async validation failed".to_string(),
});
let env = TestEnv;
let result = schema.validate_with_env_parallel(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_failure());
}
#[test]
fn test_parallel_sync_fail_skips_async() {
let schema = Schema::string()
.min_len(10)
.async_custom(AlwaysPassValidator);
let env = TestEnv;
let result = schema.validate_with_env_parallel(&json!("hi"), &JsonPath::root(), &env);
assert!(result.is_failure());
}
#[test]
fn test_parallel_multiple_async_validators() {
let schema = Schema::string()
.min_len(3)
.async_custom(AlwaysFailValidator {
message: "first error".to_string(),
})
.async_custom(AlwaysFailValidator {
message: "second error".to_string(),
})
.async_custom(AlwaysPassValidator);
let env = TestEnv;
let result = schema.validate_with_env_parallel(&json!("hello"), &JsonPath::root(), &env);
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 2);
}
}
}