pub fn required(msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let msg = msg.into();
move |value| {
if value.trim().is_empty() {
Err(msg.clone())
} else {
Ok(())
}
}
}
pub fn min_len(n: usize, msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let msg = msg.into();
move |value| {
if value.chars().count() >= n {
Ok(())
} else {
Err(msg.clone())
}
}
}
pub fn max_len(n: usize, msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let msg = msg.into();
move |value| {
if value.chars().count() <= n {
Ok(())
} else {
Err(msg.clone())
}
}
}
pub fn email() -> impl Fn(&str) -> Result<(), String> {
move |value| {
if value.chars().any(char::is_whitespace) {
return Err("invalid email".to_string());
}
let mut parts = value.split('@');
let local = parts.next().unwrap_or("");
let domain = parts.next().unwrap_or("");
if parts.next().is_some() || local.is_empty() || domain.is_empty() {
return Err("invalid email".to_string());
}
match domain.rsplit_once('.') {
Some((host, tld)) if !host.is_empty() && !tld.is_empty() => Ok(()),
_ => Err("invalid email".to_string()),
}
}
}
pub fn range_i64(lo: i64, hi: i64, msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let msg = msg.into();
move |value| match value.trim().parse::<i64>() {
Ok(n) if (lo..=hi).contains(&n) => Ok(()),
_ => Err(msg.clone()),
}
}
pub fn range_f64(lo: f64, hi: f64, msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let msg = msg.into();
move |value| match value.trim().parse::<f64>() {
Ok(n) if n.is_finite() && n >= lo && n <= hi => Ok(()),
_ => Err(msg.clone()),
}
}
pub fn one_of(allowed: &[&str], msg: impl Into<String>) -> impl Fn(&str) -> Result<(), String> {
let allowed: Vec<String> = allowed.iter().map(|s| s.to_string()).collect();
let msg = msg.into();
move |value| {
if allowed.iter().any(|a| a == value) {
Ok(())
} else {
Err(msg.clone())
}
}
}
pub fn regex(
pattern: impl Into<String>,
msg: impl Into<String>,
) -> impl Fn(&str) -> Result<(), String> {
let mut pattern = pattern.into();
let msg = msg.into();
if let Some(stripped) = pattern.strip_prefix('^') {
pattern = stripped.to_string();
}
if let Some(stripped) = pattern.strip_suffix('$') {
pattern = stripped.to_string();
}
let pattern: Vec<char> = pattern.chars().collect();
move |value| {
let input: Vec<char> = value.chars().collect();
if glob_match(&pattern, &input) {
Ok(())
} else {
Err(msg.clone())
}
}
}
fn glob_match(pattern: &[char], input: &[char]) -> bool {
match pattern.split_first() {
None => input.is_empty(),
Some(('*', rest)) => {
for skip in 0..=input.len() {
if glob_match(rest, &input[skip..]) {
return true;
}
}
false
}
Some(('.', rest)) => !input.is_empty() && glob_match(rest, &input[1..]),
Some((lit, rest)) => match input.split_first() {
Some((head, tail)) if head == lit => glob_match(rest, tail),
_ => false,
},
}
}