use std::cell::RefCell;
use std::collections::HashMap;
use stillwater::Validation;
use crate::error::{ConfigError, ConfigErrors, ConfigValidation, SourceLocation};
pub type SourceLocationMap = HashMap<String, SourceLocation>;
#[derive(Debug, Default)]
pub struct ValidationContext {
locations: SourceLocationMap,
}
impl ValidationContext {
pub fn new(locations: SourceLocationMap) -> Self {
Self { locations }
}
pub fn location_for(&self, path: &str) -> Option<&SourceLocation> {
self.locations.get(path)
}
}
thread_local! {
static VALIDATION_CONTEXT: RefCell<Option<ValidationContext>> = const { RefCell::new(None) };
static PATH_PREFIX: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn with_validation_context<F, R>(ctx: ValidationContext, f: F) -> R
where
F: FnOnce() -> R,
{
VALIDATION_CONTEXT.with(|cell| {
*cell.borrow_mut() = Some(ctx);
});
let result = f();
VALIDATION_CONTEXT.with(|cell| {
*cell.borrow_mut() = None;
});
result
}
pub fn current_source_location(path: &str) -> Option<SourceLocation> {
let full_path = PATH_PREFIX.with(|cell| {
let prefixes = cell.borrow();
if prefixes.is_empty() {
path.to_string()
} else {
format!("{}.{}", prefixes.join("."), path)
}
});
VALIDATION_CONTEXT.with(|cell| {
cell.borrow()
.as_ref()
.and_then(|ctx| ctx.location_for(&full_path).cloned())
})
}
pub fn push_path_prefix(prefix: &str) {
PATH_PREFIX.with(|cell| {
cell.borrow_mut().push(prefix.to_string());
});
}
pub fn pop_path_prefix() {
PATH_PREFIX.with(|cell| {
cell.borrow_mut().pop();
});
}
pub trait Validate {
fn validate(&self) -> ConfigValidation<()>;
fn validate_at(&self, path: &str) -> ConfigValidation<()> {
push_path_prefix(path);
let result = self.validate();
pop_path_prefix();
result.map_err(|errors| errors.with_path_prefix(path))
}
}
impl Validate for () {
fn validate(&self) -> ConfigValidation<()> {
Validation::Success(())
}
}
impl<T: Validate> Validate for Option<T> {
fn validate(&self) -> ConfigValidation<()> {
match self {
Some(inner) => inner.validate(),
None => Validation::Success(()),
}
}
}
impl<T: Validate> Validate for Vec<T> {
fn validate(&self) -> ConfigValidation<()> {
if self.is_empty() {
return Validation::Success(());
}
let validations: Vec<ConfigValidation<()>> = self
.iter()
.enumerate()
.map(|(i, item)| item.validate_at(&format!("[{}]", i)))
.collect();
Validation::all_vec(validations).map(|_| ())
}
}
macro_rules! impl_validate_noop {
($($t:ty),*) => {
$(
impl Validate for $t {
fn validate(&self) -> ConfigValidation<()> {
Validation::Success(())
}
}
)*
};
}
impl_validate_noop!(
bool, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, char, String
);
impl Validate for &str {
fn validate(&self) -> ConfigValidation<()> {
Validation::Success(())
}
}
impl Validate for std::path::PathBuf {
fn validate(&self) -> ConfigValidation<()> {
Validation::Success(())
}
}
pub trait Validator<T: ?Sized> {
fn validate(&self, value: &T, path: &str) -> ConfigValidation<()>;
}
fn fail(error: ConfigError) -> ConfigValidation<()> {
Validation::Failure(ConfigErrors::single(error))
}
use stillwater::predicate::Predicate;
pub fn from_predicate<T>(predicate: impl Predicate<T>) -> impl Validator<T>
where
T: ?Sized,
{
PredicateValidator(predicate)
}
struct PredicateValidator<P>(P);
impl<T, P> Validator<T> for PredicateValidator<P>
where
T: ?Sized,
P: Predicate<T>,
{
fn validate(&self, value: &T, path: &str) -> ConfigValidation<()> {
if self.0.check(value) {
Validation::Success(())
} else {
let source_location = current_source_location(path);
Validation::Failure(ConfigErrors::single(ConfigError::ValidationError {
path: path.to_string(),
source_location,
value: None, message: "validation failed".to_string(),
}))
}
}
}
pub fn validate_with_predicate<T>(
value: &T,
path: &str,
predicate: impl Predicate<T>,
message: impl Into<String>,
) -> ConfigValidation<()>
where
T: ?Sized,
{
if predicate.check(value) {
Validation::Success(())
} else {
let source_location = current_source_location(path);
Validation::Failure(ConfigErrors::single(ConfigError::ValidationError {
path: path.to_string(),
source_location,
value: None,
message: message.into(),
}))
}
}
pub fn validate_field<T>(
value: &T,
path: &str,
validators: &[&dyn Validator<T>],
) -> ConfigValidation<()>
where
T: ?Sized,
{
if validators.is_empty() {
return Validation::Success(());
}
let results: Vec<ConfigValidation<()>> =
validators.iter().map(|v| v.validate(value, path)).collect();
Validation::all_vec(results).map(|_| ())
}
pub fn validate_nested<T: Validate>(value: &T, path: &str) -> ConfigValidation<()> {
value.validate_at(path)
}
pub fn validate_optional_nested<T: Validate>(
value: &Option<T>,
path: &str,
) -> ConfigValidation<()> {
match value {
Some(v) => v.validate_at(path),
None => Validation::Success(()),
}
}
pub mod validators {
use super::*;
use std::fmt::Display;
use std::ops::RangeInclusive;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub struct NonEmpty;
impl Validator<str> for NonEmpty {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
if value.is_empty() {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(String::new()),
message: "value cannot be empty".to_string(),
})
} else {
Validation::Success(())
}
}
}
impl Validator<String> for NonEmpty {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<NonEmpty as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinLength(pub usize);
impl Validator<str> for MinLength {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
if value.len() < self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!("length {} is less than minimum {}", value.len(), self.0),
})
} else {
Validation::Success(())
}
}
}
impl Validator<String> for MinLength {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<MinLength as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaxLength(pub usize);
impl Validator<str> for MaxLength {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
if value.len() > self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!("length {} exceeds maximum {}", value.len(), self.0),
})
} else {
Validation::Success(())
}
}
}
impl Validator<String> for MaxLength {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<MaxLength as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone)]
pub struct Length(pub RangeInclusive<usize>);
impl Validator<str> for Length {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
let len = value.len();
if !self.0.contains(&len) {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!(
"length {} is not in range {}..={}",
len,
self.0.start(),
self.0.end()
),
})
} else {
Validation::Success(())
}
}
}
impl Validator<String> for Length {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<Length as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone)]
pub struct Pattern(pub String);
impl Pattern {
pub fn new(pattern: impl Into<String>) -> Self {
Self(pattern.into())
}
}
impl Validator<str> for Pattern {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
match regex::Regex::new(&self.0) {
Ok(re) => {
if re.is_match(value) {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!("value does not match pattern '{}'", self.0),
})
}
}
Err(e) => fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!("invalid pattern '{}': {}", self.0, e),
}),
}
}
}
impl Validator<String> for Pattern {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<Pattern as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Email;
impl Validator<str> for Email {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
let email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
let re = regex::Regex::new(email_pattern).expect("valid email regex");
if re.is_match(value) {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value is not a valid email address".to_string(),
})
}
}
}
impl Validator<String> for Email {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<Email as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Url;
impl Validator<str> for Url {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
let url_pattern = r"^(https?|ftp)://[^\s/$.?#].[^\s]*$";
let re = regex::Regex::new(url_pattern).expect("valid url regex");
if re.is_match(value) {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value is not a valid URL".to_string(),
})
}
}
}
impl Validator<String> for Url {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<Url as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone)]
pub struct Range<T>(pub RangeInclusive<T>);
impl<T> Validator<T> for Range<T>
where
T: PartialOrd + Display + Clone,
{
fn validate(&self, value: &T, path: &str) -> ConfigValidation<()> {
if !self.0.contains(value) {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: format!(
"value {} is not in range {}..={}",
value,
self.0.start(),
self.0.end()
),
})
} else {
Validation::Success(())
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Positive;
macro_rules! impl_positive_for_signed {
($($t:ty),*) => {
$(
impl Validator<$t> for Positive {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value > 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be positive".to_string(),
})
}
}
}
)*
};
}
macro_rules! impl_positive_for_unsigned {
($($t:ty),*) => {
$(
impl Validator<$t> for Positive {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value > 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be positive".to_string(),
})
}
}
}
)*
};
}
macro_rules! impl_positive_for_float {
($($t:ty),*) => {
$(
impl Validator<$t> for Positive {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value > 0.0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be positive".to_string(),
})
}
}
}
)*
};
}
impl_positive_for_signed!(i8, i16, i32, i64, i128, isize);
impl_positive_for_unsigned!(u8, u16, u32, u64, u128, usize);
impl_positive_for_float!(f32, f64);
#[derive(Debug, Clone, Copy)]
pub struct Negative;
macro_rules! impl_negative_for_signed {
($($t:ty),*) => {
$(
impl Validator<$t> for Negative {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value < 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be negative".to_string(),
})
}
}
}
)*
};
}
macro_rules! impl_negative_for_float {
($($t:ty),*) => {
$(
impl Validator<$t> for Negative {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value < 0.0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be negative".to_string(),
})
}
}
}
)*
};
}
impl_negative_for_signed!(i8, i16, i32, i64, i128, isize);
impl_negative_for_float!(f32, f64);
#[derive(Debug, Clone, Copy)]
pub struct NonZero;
macro_rules! impl_nonzero_for_int {
($($t:ty),*) => {
$(
impl Validator<$t> for NonZero {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
if *value != 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value cannot be zero".to_string(),
})
}
}
}
)*
};
}
macro_rules! impl_nonzero_for_float {
($($t:ty),*) => {
$(
impl Validator<$t> for NonZero {
fn validate(&self, value: &$t, path: &str) -> ConfigValidation<()> {
#[allow(clippy::float_cmp)]
if *value != 0.0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value cannot be zero".to_string(),
})
}
}
}
)*
};
}
impl_nonzero_for_int!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
impl_nonzero_for_float!(f32, f64);
#[derive(Debug, Clone, Copy)]
pub struct NonEmptyCollection;
impl<T> Validator<Vec<T>> for NonEmptyCollection {
fn validate(&self, value: &Vec<T>, path: &str) -> ConfigValidation<()> {
if value.is_empty() {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some("[]".to_string()),
message: "collection cannot be empty".to_string(),
})
} else {
Validation::Success(())
}
}
}
impl<T> Validator<[T]> for NonEmptyCollection {
fn validate(&self, value: &[T], path: &str) -> ConfigValidation<()> {
if value.is_empty() {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some("[]".to_string()),
message: "collection cannot be empty".to_string(),
})
} else {
Validation::Success(())
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinItems(pub usize);
impl<T> Validator<Vec<T>> for MinItems {
fn validate(&self, value: &Vec<T>, path: &str) -> ConfigValidation<()> {
if value.len() < self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(format!("[{} items]", value.len())),
message: format!(
"collection has {} items, minimum is {}",
value.len(),
self.0
),
})
} else {
Validation::Success(())
}
}
}
impl<T> Validator<[T]> for MinItems {
fn validate(&self, value: &[T], path: &str) -> ConfigValidation<()> {
if value.len() < self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(format!("[{} items]", value.len())),
message: format!(
"collection has {} items, minimum is {}",
value.len(),
self.0
),
})
} else {
Validation::Success(())
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaxItems(pub usize);
impl<T> Validator<Vec<T>> for MaxItems {
fn validate(&self, value: &Vec<T>, path: &str) -> ConfigValidation<()> {
if value.len() > self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(format!("[{} items]", value.len())),
message: format!(
"collection has {} items, maximum is {}",
value.len(),
self.0
),
})
} else {
Validation::Success(())
}
}
}
impl<T> Validator<[T]> for MaxItems {
fn validate(&self, value: &[T], path: &str) -> ConfigValidation<()> {
if value.len() > self.0 {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(format!("[{} items]", value.len())),
message: format!(
"collection has {} items, maximum is {}",
value.len(),
self.0
),
})
} else {
Validation::Success(())
}
}
}
#[derive(Debug, Clone)]
pub struct Each<V>(pub V);
impl<V, T> Validator<Vec<T>> for Each<V>
where
V: Validator<T>,
{
fn validate(&self, value: &Vec<T>, path: &str) -> ConfigValidation<()> {
if value.is_empty() {
return Validation::Success(());
}
let results: Vec<ConfigValidation<()>> = value
.iter()
.enumerate()
.map(|(i, item)| self.0.validate(item, &format!("{}[{}]", path, i)))
.collect();
Validation::all_vec(results).map(|_| ())
}
}
impl<V, T> Validator<[T]> for Each<V>
where
V: Validator<T>,
{
fn validate(&self, value: &[T], path: &str) -> ConfigValidation<()> {
if value.is_empty() {
return Validation::Success(());
}
let results: Vec<ConfigValidation<()>> = value
.iter()
.enumerate()
.map(|(i, item)| self.0.validate(item, &format!("{}[{}]", path, i)))
.collect();
Validation::all_vec(results).map(|_| ())
}
}
#[derive(Debug, Clone, Copy)]
pub struct FileExists;
impl Validator<Path> for FileExists {
fn validate(&self, value: &Path, path: &str) -> ConfigValidation<()> {
if value.is_file() {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: "file does not exist".to_string(),
})
}
}
}
impl Validator<std::path::PathBuf> for FileExists {
fn validate(&self, value: &std::path::PathBuf, path: &str) -> ConfigValidation<()> {
<FileExists as Validator<Path>>::validate(self, value.as_path(), path)
}
}
impl Validator<str> for FileExists {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
<FileExists as Validator<Path>>::validate(self, Path::new(value), path)
}
}
impl Validator<String> for FileExists {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<FileExists as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DirExists;
impl Validator<Path> for DirExists {
fn validate(&self, value: &Path, path: &str) -> ConfigValidation<()> {
if value.is_dir() {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: "directory does not exist".to_string(),
})
}
}
}
impl Validator<std::path::PathBuf> for DirExists {
fn validate(&self, value: &std::path::PathBuf, path: &str) -> ConfigValidation<()> {
<DirExists as Validator<Path>>::validate(self, value.as_path(), path)
}
}
impl Validator<str> for DirExists {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
<DirExists as Validator<Path>>::validate(self, Path::new(value), path)
}
}
impl Validator<String> for DirExists {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<DirExists as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ParentExists;
impl Validator<Path> for ParentExists {
fn validate(&self, value: &Path, path: &str) -> ConfigValidation<()> {
match value.parent() {
Some(parent) if parent.is_dir() || parent.as_os_str().is_empty() => {
Validation::Success(())
}
Some(_) => fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: "parent directory does not exist".to_string(),
}),
None => fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: "path has no parent directory".to_string(),
}),
}
}
}
impl Validator<std::path::PathBuf> for ParentExists {
fn validate(&self, value: &std::path::PathBuf, path: &str) -> ConfigValidation<()> {
<ParentExists as Validator<Path>>::validate(self, value.as_path(), path)
}
}
impl Validator<str> for ParentExists {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
<ParentExists as Validator<Path>>::validate(self, Path::new(value), path)
}
}
impl Validator<String> for ParentExists {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<ParentExists as Validator<str>>::validate(self, value.as_str(), path)
}
}
#[derive(Debug, Clone)]
pub struct Extension(pub String);
impl Extension {
pub fn new(ext: impl Into<String>) -> Self {
Self(ext.into())
}
}
impl Validator<Path> for Extension {
fn validate(&self, value: &Path, path: &str) -> ConfigValidation<()> {
match value.extension() {
Some(ext) if ext == self.0.as_str() => Validation::Success(()),
Some(ext) => fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: format!(
"expected extension '{}', found '{}'",
self.0,
ext.to_string_lossy()
),
}),
None => fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.display().to_string()),
message: format!("expected extension '{}', found none", self.0),
}),
}
}
}
impl Validator<std::path::PathBuf> for Extension {
fn validate(&self, value: &std::path::PathBuf, path: &str) -> ConfigValidation<()> {
<Extension as Validator<Path>>::validate(self, value.as_path(), path)
}
}
impl Validator<str> for Extension {
fn validate(&self, value: &str, path: &str) -> ConfigValidation<()> {
<Extension as Validator<Path>>::validate(self, Path::new(value), path)
}
}
impl Validator<String> for Extension {
fn validate(&self, value: &String, path: &str) -> ConfigValidation<()> {
<Extension as Validator<str>>::validate(self, value.as_str(), path)
}
}
}
pub fn custom<T, F>(f: F) -> impl Validator<T>
where
F: Fn(&T, &str) -> ConfigValidation<()>,
{
struct Custom<F>(F);
impl<T, F> Validator<T> for Custom<F>
where
F: Fn(&T, &str) -> ConfigValidation<()>,
{
fn validate(&self, value: &T, path: &str) -> ConfigValidation<()> {
(self.0)(value, path)
}
}
Custom(f)
}
pub struct When<V, F> {
validator: V,
condition: F,
}
impl<V, F> When<V, F> {
pub fn new(validator: V, condition: F) -> Self {
Self {
validator,
condition,
}
}
}
impl<V, F, T> Validator<T> for When<V, F>
where
V: Validator<T>,
F: Fn() -> bool,
T: ?Sized,
{
fn validate(&self, value: &T, path: &str) -> ConfigValidation<()> {
if (self.condition)() {
self.validator.validate(value, path)
} else {
Validation::Success(())
}
}
}
#[cfg(test)]
mod tests {
use super::validators::*;
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn test_unit_validate() {
let result = ().validate();
assert!(result.is_success());
}
#[test]
fn test_option_validate_none() {
let opt: Option<String> = None;
let result = opt.validate();
assert!(result.is_success());
}
#[test]
fn test_option_validate_some() {
let opt = Some("value".to_string());
let result = opt.validate();
assert!(result.is_success());
}
#[test]
fn test_vec_validate_empty() {
let v: Vec<String> = vec![];
let result = v.validate();
assert!(result.is_success());
}
#[test]
fn test_vec_validate_with_items() {
let v = vec!["a".to_string(), "b".to_string()];
let result = v.validate();
assert!(result.is_success());
}
#[test]
fn test_primitive_validate() {
assert!(42i32.validate().is_success());
assert!((std::f64::consts::E).validate().is_success());
assert!(true.validate().is_success());
assert!("hello".to_string().validate().is_success());
}
#[test]
fn test_pathbuf_validate() {
let path = PathBuf::from("/some/path");
assert!(path.validate().is_success());
}
struct FailingConfig;
impl Validate for FailingConfig {
fn validate(&self) -> ConfigValidation<()> {
fail(ConfigError::ValidationError {
path: "inner".to_string(),
source_location: None,
value: None,
message: "always fails".to_string(),
})
}
}
#[test]
fn test_validate_at_prefixes_path() {
let config = FailingConfig;
let result = config.validate_at("outer");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.first().path(), Some("outer.inner"));
}
}
#[test]
fn test_non_empty_success() {
let result = NonEmpty.validate("hello", "field");
assert!(result.is_success());
}
#[test]
fn test_non_empty_failure() {
let result = NonEmpty.validate("", "field");
assert!(result.is_failure());
}
#[test]
fn test_min_length_success() {
let result = MinLength(3).validate("hello", "field");
assert!(result.is_success());
}
#[test]
fn test_min_length_failure() {
let result = MinLength(10).validate("hi", "field");
assert!(result.is_failure());
}
#[test]
fn test_max_length_success() {
let result = MaxLength(10).validate("hello", "field");
assert!(result.is_success());
}
#[test]
fn test_max_length_failure() {
let result = MaxLength(3).validate("hello", "field");
assert!(result.is_failure());
}
#[test]
fn test_length_success() {
let result = Length(3..=10).validate("hello", "field");
assert!(result.is_success());
}
#[test]
fn test_length_failure_too_short() {
let result = Length(5..=10).validate("hi", "field");
assert!(result.is_failure());
}
#[test]
fn test_length_failure_too_long() {
let result = Length(1..=3).validate("hello", "field");
assert!(result.is_failure());
}
#[test]
fn test_pattern_success() {
let result = Pattern::new(r"^\d+$").validate("12345", "field");
assert!(result.is_success());
}
#[test]
fn test_pattern_failure() {
let result = Pattern::new(r"^\d+$").validate("abc", "field");
assert!(result.is_failure());
}
#[test]
fn test_email_success() {
let result = Email.validate("user@example.com", "email");
assert!(result.is_success());
}
#[test]
fn test_email_failure() {
let result = Email.validate("not-an-email", "email");
assert!(result.is_failure());
}
#[test]
fn test_url_success() {
let result = Url.validate("https://example.com", "url");
assert!(result.is_success());
}
#[test]
fn test_url_failure() {
let result = Url.validate("not-a-url", "url");
assert!(result.is_failure());
}
#[test]
fn test_range_success() {
let result = Range(1..=100).validate(&50i32, "field");
assert!(result.is_success());
}
#[test]
fn test_range_failure_below() {
let result = Range(10..=100).validate(&5i32, "field");
assert!(result.is_failure());
}
#[test]
fn test_range_failure_above() {
let result = Range(1..=10).validate(&50i32, "field");
assert!(result.is_failure());
}
#[test]
fn test_positive_success() {
assert!(Positive.validate(&42i32, "field").is_success());
assert!(Positive.validate(&1u32, "field").is_success());
assert!(Positive.validate(&0.5f64, "field").is_success());
}
#[test]
fn test_positive_failure() {
assert!(Positive.validate(&-1i32, "field").is_failure());
assert!(Positive.validate(&0i32, "field").is_failure());
assert!(Positive.validate(&0u32, "field").is_failure());
assert!(Positive.validate(&-0.5f64, "field").is_failure());
}
#[test]
fn test_negative_success() {
assert!(Negative.validate(&-42i32, "field").is_success());
assert!(Negative.validate(&-0.5f64, "field").is_success());
}
#[test]
fn test_negative_failure() {
assert!(Negative.validate(&42i32, "field").is_failure());
assert!(Negative.validate(&0i32, "field").is_failure());
assert!(Negative.validate(&0.5f64, "field").is_failure());
}
#[test]
fn test_non_zero_success() {
assert!(NonZero.validate(&42i32, "field").is_success());
assert!(NonZero.validate(&-1i32, "field").is_success());
assert!(NonZero.validate(&1u32, "field").is_success());
assert!(NonZero.validate(&0.5f64, "field").is_success());
}
#[test]
fn test_non_zero_failure() {
assert!(NonZero.validate(&0i32, "field").is_failure());
assert!(NonZero.validate(&0u32, "field").is_failure());
assert!(NonZero.validate(&0.0f64, "field").is_failure());
}
#[test]
fn test_non_empty_collection_success() {
let v = vec![1, 2, 3];
let result = NonEmptyCollection.validate(&v, "items");
assert!(result.is_success());
}
#[test]
fn test_non_empty_collection_failure() {
let v: Vec<i32> = vec![];
let result = NonEmptyCollection.validate(&v, "items");
assert!(result.is_failure());
}
#[test]
fn test_min_items_success() {
let v = vec![1, 2, 3];
let result = MinItems(2).validate(&v, "items");
assert!(result.is_success());
}
#[test]
fn test_min_items_failure() {
let v = vec![1];
let result = MinItems(3).validate(&v, "items");
assert!(result.is_failure());
}
#[test]
fn test_max_items_success() {
let v = vec![1, 2, 3];
let result = MaxItems(5).validate(&v, "items");
assert!(result.is_success());
}
#[test]
fn test_max_items_failure() {
let v = vec![1, 2, 3, 4, 5];
let result = MaxItems(3).validate(&v, "items");
assert!(result.is_failure());
}
#[test]
fn test_each_success() {
let v = vec![1, 2, 3];
let result = Each(Positive).validate(&v, "items");
assert!(result.is_success());
}
#[test]
fn test_each_failure_accumulates_errors() {
let v = vec![1, -2, -3, 4];
let result = Each(Positive).validate(&v, "items");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 2);
}
}
#[test]
fn test_each_empty_collection() {
let v: Vec<i32> = vec![];
let result = Each(Positive).validate(&v, "items");
assert!(result.is_success());
}
#[test]
fn test_extension_success() {
let result = Extension::new("toml").validate(Path::new("config.toml"), "file");
assert!(result.is_success());
}
#[test]
fn test_extension_failure_wrong() {
let result = Extension::new("toml").validate(Path::new("config.json"), "file");
assert!(result.is_failure());
}
#[test]
fn test_extension_failure_none() {
let result = Extension::new("toml").validate(Path::new("config"), "file");
assert!(result.is_failure());
}
#[test]
fn test_validate_field_empty_validators() {
let empty: &[&dyn Validator<str>] = &[];
let result = validate_field("value", "field", empty);
assert!(result.is_success());
}
#[test]
fn test_validate_field_single_validator() {
let result = validate_field("hello", "field", &[&NonEmpty]);
assert!(result.is_success());
}
#[test]
fn test_validate_field_multiple_validators_all_pass() {
let result = validate_field("hello", "field", &[&NonEmpty, &MinLength(3)]);
assert!(result.is_success());
}
#[test]
fn test_validate_field_accumulates_errors() {
let result = validate_field("", "field", &[&NonEmpty, &MinLength(3)]);
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 2);
}
}
struct InnerConfig {
value: i32,
}
impl Validate for InnerConfig {
fn validate(&self) -> ConfigValidation<()> {
if self.value > 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: "value".to_string(),
source_location: None,
value: Some(self.value.to_string()),
message: "must be positive".to_string(),
})
}
}
}
#[test]
fn test_validate_nested_success() {
let inner = InnerConfig { value: 42 };
let result = validate_nested(&inner, "config");
assert!(result.is_success());
}
#[test]
fn test_validate_nested_failure() {
let inner = InnerConfig { value: -1 };
let result = validate_nested(&inner, "config");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.first().path(), Some("config.value"));
}
}
#[test]
fn test_validate_optional_nested_none() {
let opt: Option<InnerConfig> = None;
let result = validate_optional_nested(&opt, "config");
assert!(result.is_success());
}
#[test]
fn test_validate_optional_nested_some() {
let opt = Some(InnerConfig { value: -1 });
let result = validate_optional_nested(&opt, "config");
assert!(result.is_failure());
}
#[test]
fn test_custom_validator() {
let even_validator = custom(|value: &i32, path: &str| {
if value % 2 == 0 {
Validation::Success(())
} else {
fail(ConfigError::ValidationError {
path: path.to_string(),
source_location: None,
value: Some(value.to_string()),
message: "value must be even".to_string(),
})
}
});
assert!(even_validator.validate(&4, "num").is_success());
assert!(even_validator.validate(&3, "num").is_failure());
}
#[test]
fn test_when_condition_true() {
let validator = When::new(NonEmpty, || true);
let result = validator.validate("", "field");
assert!(result.is_failure());
}
#[test]
fn test_when_condition_false() {
let validator = When::new(NonEmpty, || false);
let result = validator.validate("", "field");
assert!(result.is_success());
}
struct DatabaseConfig {
host: String,
port: u16,
pool_size: u32,
}
impl Validate for DatabaseConfig {
fn validate(&self) -> ConfigValidation<()> {
let validations = vec![
validate_field(&self.host, "host", &[&NonEmpty]),
validate_field(&self.port, "port", &[&Range(1..=65535)]),
validate_field(&self.pool_size, "pool_size", &[&Range(1..=100)]),
];
Validation::all_vec(validations).map(|_| ())
}
}
#[test]
fn test_database_config_valid() {
let config = DatabaseConfig {
host: "localhost".to_string(),
port: 5432,
pool_size: 10,
};
assert!(config.validate().is_success());
}
#[test]
fn test_database_config_accumulates_all_errors() {
let config = DatabaseConfig {
host: "".to_string(),
port: 0,
pool_size: 200,
};
let result = config.validate();
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 3);
}
}
#[test]
fn test_vec_custom_validate() {
let configs = vec![
InnerConfig { value: 1 },
InnerConfig { value: -1 },
InnerConfig { value: -2 },
];
let result = configs.validate();
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 2);
let paths: Vec<_> = errors.iter().filter_map(|e| e.path()).collect();
assert!(paths.contains(&"[1].value"));
assert!(paths.contains(&"[2].value"));
}
}
#[test]
fn test_validation_context_lookup() {
let mut locations = SourceLocationMap::new();
locations.insert(
"host".to_string(),
SourceLocation::new("config.toml").with_line(5),
);
locations.insert(
"port".to_string(),
SourceLocation::new("config.toml").with_line(6),
);
let ctx = ValidationContext::new(locations);
let host_loc = ctx.location_for("host").unwrap();
assert_eq!(host_loc.source, "config.toml");
assert_eq!(host_loc.line, Some(5));
let port_loc = ctx.location_for("port").unwrap();
assert_eq!(port_loc.line, Some(6));
assert!(ctx.location_for("missing").is_none());
}
#[test]
fn test_with_validation_context() {
let mut locations = SourceLocationMap::new();
locations.insert(
"test_field".to_string(),
SourceLocation::new("test.toml").with_line(10),
);
let ctx = ValidationContext::new(locations);
assert!(current_source_location("test_field").is_none());
let result = with_validation_context(ctx, || {
let loc = current_source_location("test_field");
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.source, "test.toml");
assert_eq!(loc.line, Some(10));
"success"
});
assert_eq!(result, "success");
assert!(current_source_location("test_field").is_none());
}
#[test]
fn test_context_clears_on_completion() {
let mut locations = SourceLocationMap::new();
locations.insert("field".to_string(), SourceLocation::new("a.toml"));
let ctx = ValidationContext::new(locations);
with_validation_context(ctx, || ());
assert!(current_source_location("field").is_none());
}
#[test]
fn test_path_prefix_for_nested_lookup() {
let mut locations = SourceLocationMap::new();
locations.insert(
"server.host".to_string(),
SourceLocation::new("config.toml").with_line(3),
);
locations.insert(
"server.port".to_string(),
SourceLocation::new("config.toml").with_line(4),
);
locations.insert(
"database.host".to_string(),
SourceLocation::new("config.toml").with_line(7),
);
let ctx = ValidationContext::new(locations);
with_validation_context(ctx, || {
assert!(current_source_location("host").is_none());
push_path_prefix("server");
let loc = current_source_location("host");
assert!(loc.is_some());
let loc = loc.unwrap();
assert_eq!(loc.source, "config.toml");
assert_eq!(loc.line, Some(3));
let port_loc = current_source_location("port").unwrap();
assert_eq!(port_loc.line, Some(4));
pop_path_prefix();
assert!(current_source_location("host").is_none());
push_path_prefix("database");
let db_loc = current_source_location("host").unwrap();
assert_eq!(db_loc.line, Some(7));
pop_path_prefix();
});
}
#[test]
fn test_nested_path_prefix_stacking() {
let mut locations = SourceLocationMap::new();
locations.insert(
"outer.inner.field".to_string(),
SourceLocation::new("config.toml").with_line(10),
);
let ctx = ValidationContext::new(locations);
with_validation_context(ctx, || {
assert!(current_source_location("field").is_none());
push_path_prefix("outer");
assert!(current_source_location("field").is_none());
push_path_prefix("inner");
let loc = current_source_location("field");
assert!(loc.is_some());
assert_eq!(loc.unwrap().line, Some(10));
pop_path_prefix();
assert!(current_source_location("field").is_none());
pop_path_prefix();
});
}
use stillwater::predicate::prelude::*;
#[test]
fn test_from_predicate_success() {
let validator = from_predicate(not_empty());
let result = validator.validate("hello", "field");
assert!(result.is_success());
}
#[test]
fn test_from_predicate_failure() {
let validator = from_predicate(not_empty());
let result = validator.validate("", "field");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().path(), Some("field"));
}
}
#[test]
fn test_validate_with_predicate_success() {
let result = validate_with_predicate(
&42,
"port",
between(1, 65535),
"port must be between 1 and 65535",
);
assert!(result.is_success());
}
#[test]
fn test_validate_with_predicate_failure() {
let result = validate_with_predicate(
&0,
"port",
between(1, 65535),
"port must be between 1 and 65535",
);
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().path(), Some("port"));
if let ConfigError::ValidationError { message, .. } = errors.first() {
assert_eq!(message, "port must be between 1 and 65535");
} else {
panic!("Expected ValidationError");
}
}
}
#[test]
fn test_validate_with_predicate_custom_message() {
let result = validate_with_predicate("", "username", not_empty(), "username is required");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
if let ConfigError::ValidationError { message, .. } = errors.first() {
assert_eq!(message, "username is required");
} else {
panic!("Expected ValidationError");
}
}
}
#[test]
fn test_predicate_preserves_source_location() {
let mut locations = SourceLocationMap::new();
locations.insert(
"host".to_string(),
SourceLocation::new("config.toml").with_line(5),
);
let ctx = ValidationContext::new(locations);
with_validation_context(ctx, || {
let result = validate_with_predicate("", "host", not_empty(), "host cannot be empty");
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
let err = errors.first();
assert_eq!(
err.source_location().map(|l| l.source.as_str()),
Some("config.toml")
);
assert_eq!(err.source_location().and_then(|l| l.line), Some(5));
}
});
}
#[test]
fn test_predicate_with_nested_validation() {
let mut locations = SourceLocationMap::new();
locations.insert(
"database.host".to_string(),
SourceLocation::new("config.toml").with_line(10),
);
let ctx = ValidationContext::new(locations);
with_validation_context(ctx, || {
push_path_prefix("database");
let result = validate_with_predicate("", "host", not_empty(), "host required");
pop_path_prefix();
assert!(result.is_failure());
if let Validation::Failure(errors) = result {
let err = errors.first();
assert_eq!(err.source_location().and_then(|l| l.line), Some(10));
}
});
}
#[test]
fn test_complex_string_predicate() {
assert!(validate_with_predicate(
"user123",
"username",
len_min(3),
"must be at least 3 chars"
)
.is_success());
assert!(
validate_with_predicate("ab", "username", len_min(3), "must be at least 3 chars")
.is_failure()
);
assert!(validate_with_predicate(
"hello",
"field",
len_between(3, 10),
"length must be 3-10 chars"
)
.is_success());
assert!(validate_with_predicate(
"hi",
"field",
len_between(3, 10),
"length must be 3-10 chars"
)
.is_failure());
}
#[test]
fn test_numeric_range_predicate() {
let pred = gt(0).and(le(65535));
let validator = from_predicate(pred);
assert!(validator.validate(&1, "port").is_success());
assert!(validator.validate(&8080, "port").is_success());
assert!(validator.validate(&65535, "port").is_success());
assert!(validator.validate(&0, "port").is_failure());
assert!(validator.validate(&65536, "port").is_failure());
assert!(validator.validate(&-1, "port").is_failure());
}
#[test]
fn test_string_length_predicates() {
assert!(validate_with_predicate("hello", "field", len_min(3), "min 3 chars").is_success());
assert!(validate_with_predicate("hi", "field", len_min(3), "min 3 chars").is_failure());
assert!(
validate_with_predicate("hello", "field", len_max(10), "max 10 chars").is_success()
);
assert!(
validate_with_predicate("verylongstring", "field", len_max(10), "max 10 chars")
.is_failure()
);
assert!(
validate_with_predicate("hello", "field", len_eq(5), "exactly 5 chars").is_success()
);
assert!(validate_with_predicate("hi", "field", len_eq(5), "exactly 5 chars").is_failure());
}
#[test]
fn test_predicate_and_validator_together() {
let pred_validator = from_predicate(len_min(3));
let trad_validator = NonEmpty;
let result = validate_field("hello", "field", &[&trad_validator, &pred_validator]);
assert!(result.is_success());
let result = validate_field("", "field", &[&trad_validator, &pred_validator]);
assert!(result.is_failure());
let result = validate_field("hi", "field", &[&trad_validator, &pred_validator]);
assert!(result.is_failure());
}
#[test]
fn test_validate_field_with_predicate() {
let validator = from_predicate(between(1, 100));
let result = validate_field(&50, "value", &[&validator]);
assert!(result.is_success());
let result = validate_field(&150, "value", &[&validator]);
assert!(result.is_failure());
}
}