use std::fmt::Display;
use regex::Regex;
use crate::error::ValidationError;
pub type BoxedStringValidator =
Box<dyn Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static>;
pub type BoxedValueValidator<T> =
Box<dyn Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static>;
#[must_use = "pass validators to a variable spec"]
pub fn min_length(
minimum: usize,
) -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
if value.chars().count() >= minimum {
Ok(())
} else {
Err(ValidationError::new(format!(
"length must be at least {minimum}"
)))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn max_length(
maximum: usize,
) -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
let length = value.chars().count();
if length <= maximum {
Ok(())
} else {
Err(ValidationError::new(format!(
"length must be at most {maximum}"
)))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn one_of<const N: usize>(
allowed: [&'static str; N],
) -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
if allowed.contains(&value) {
Ok(())
} else {
Err(ValidationError::new("value must be in the allowed set"))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn one_of_values<T, const N: usize>(
allowed: [T; N],
) -> impl Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static
where
T: PartialEq + Copy + Send + Sync + 'static,
{
move |value| {
if allowed.contains(&value) {
Ok(())
} else {
Err(ValidationError::new("value must be in the allowed set"))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn in_range<T>(
minimum: T,
maximum: T,
) -> impl Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static
where
T: PartialOrd + Copy + Display + Send + Sync + 'static,
{
move |value| {
if value >= minimum && value <= maximum {
Ok(())
} else {
Err(ValidationError::new(format!(
"value must be between {minimum} and {maximum}"
)))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn min_value<T>(minimum: T) -> impl Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static
where
T: PartialOrd + Copy + Display + Send + Sync + 'static,
{
move |value| {
if value >= minimum {
Ok(())
} else {
Err(ValidationError::new(format!(
"value must be at least {minimum}"
)))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn max_value<T>(maximum: T) -> impl Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static
where
T: PartialOrd + Copy + Display + Send + Sync + 'static,
{
move |value| {
if value <= maximum {
Ok(())
} else {
Err(ValidationError::new(format!(
"value must be at most {maximum}"
)))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn matches_pattern(pattern: &str) -> Result<BoxedStringValidator, regex::Error> {
let regex = Regex::new(pattern)?;
let pattern = pattern.to_owned();
Ok(Box::new(move |value| {
if regex.is_match(value) {
Ok(())
} else {
Err(ValidationError::new(format!(
"value must match pattern {pattern}"
)))
}
}))
}
#[must_use = "pass validators to a variable spec"]
pub fn is_url() -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
is_url_with_options(true, ["http", "https"])
}
#[must_use = "pass validators to a variable spec"]
pub fn is_url_with_options<I, S>(
require_scheme: bool,
allowed_schemes: I,
) -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let allowed_schemes = allowed_schemes
.into_iter()
.map(|scheme| scheme.into().to_ascii_lowercase())
.collect::<Vec<_>>();
move |value| validate_url(value, require_scheme, &allowed_schemes)
}
#[must_use = "pass validators to a variable spec"]
pub fn is_email() -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
let regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
.map_err(|_| ValidationError::new("email validator is unavailable"))?;
if regex.is_match(value) {
Ok(())
} else {
Err(ValidationError::new("value must be an email address"))
}
}
}
#[must_use = "pass validators to a variable spec"]
pub fn all_of_str(
validators: Vec<BoxedStringValidator>,
) -> impl Fn(&str) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
for validator in &validators {
validator(value)?;
}
Ok(())
}
}
#[must_use = "pass validators to a variable spec"]
pub fn all_of<T>(
validators: Vec<BoxedValueValidator<T>>,
) -> impl Fn(T) -> Result<(), ValidationError> + Send + Sync + 'static
where
T: Copy + Send + Sync + 'static,
{
move |value| {
for validator in &validators {
validator(value)?;
}
Ok(())
}
}
#[must_use = "pass validators to a variable spec"]
pub fn u16_in_range(
minimum: u16,
maximum: u16,
) -> impl Fn(u16) -> Result<(), ValidationError> + Send + Sync + 'static {
move |value| {
if (minimum..=maximum).contains(&value) {
Ok(())
} else {
Err(ValidationError::new(format!(
"value must be between {minimum} and {maximum}"
)))
}
}
}
fn validate_url(
value: &str,
require_scheme: bool,
allowed_schemes: &[String],
) -> Result<(), ValidationError> {
let (scheme, rest) = match value.split_once("://") {
Some((scheme, rest)) => (Some(scheme.to_ascii_lowercase()), rest),
None if require_scheme => {
return Err(ValidationError::new("URL must include a scheme"));
}
None => (None, value.trim_start_matches("//")),
};
if let Some(scheme) = scheme {
if !allowed_schemes.contains(&scheme) {
return Err(ValidationError::new("URL scheme is not allowed"));
}
}
let host = rest.split(['/', '?', '#']).next().unwrap_or_default();
if host.is_empty() {
return Err(ValidationError::new("URL must include a host"));
}
if host.chars().any(char::is_whitespace) {
return Err(ValidationError::new("URL host must not contain whitespace"));
}
Ok(())
}