use crate::ConfigError;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::PathBuf;
#[cfg(feature = "regex")]
pub fn validate_regex<F: Fn() -> String>(
field: F,
pattern: ®ex::Regex,
value: &str,
) -> Result<(), ConfigError> {
if pattern.is_match(value) {
Ok(())
} else {
Err(ConfigError::ConfigParseError(
field(),
format!("regex mismatch: {}", pattern.as_str()),
))
}
}
pub fn validate_range<T: PartialOrd, F: Fn() -> String>(
field: F,
value: &T,
min: Option<&T>,
max: Option<&T>,
) -> Result<(), ConfigError> {
if let Some(min) = min {
match value.partial_cmp(min) {
Some(std::cmp::Ordering::Less) => {
return Err(ConfigError::ConfigParseError(
field(),
"value must be >= min".to_string(),
));
}
Some(_) => {}
None => {
return Err(ConfigError::ConfigParseError(
field(),
"value is not comparable to min".to_string(),
));
}
}
}
if let Some(max) = max {
match value.partial_cmp(max) {
Some(std::cmp::Ordering::Greater) => {
return Err(ConfigError::ConfigParseError(
field(),
"value must be <= max".to_string(),
));
}
None => {
return Err(ConfigError::ConfigParseError(
field(),
"value is not comparable to max".to_string(),
));
}
_ => {}
}
}
Ok(())
}
pub trait ValidateLength {
fn validate_len(&self) -> usize;
}
impl ValidateLength for String {
fn validate_len(&self) -> usize {
self.chars().count()
}
}
impl<T> ValidateLength for Vec<T> {
fn validate_len(&self) -> usize {
self.len()
}
}
impl<K, V> ValidateLength for HashMap<K, V> {
fn validate_len(&self) -> usize {
self.len()
}
}
impl<K, V> ValidateLength for BTreeMap<K, V> {
fn validate_len(&self) -> usize {
self.len()
}
}
impl ValidateLength for OsString {
fn validate_len(&self) -> usize {
self.to_string_lossy().chars().count()
}
}
impl ValidateLength for PathBuf {
fn validate_len(&self) -> usize {
self.as_os_str().to_string_lossy().chars().count()
}
}
pub fn validate_length<T: ValidateLength, F: Fn() -> String>(
field: F,
value: &T,
min: Option<usize>,
max: Option<usize>,
) -> Result<(), ConfigError> {
let len = value.validate_len();
if let Some(min) = min {
if len < min {
return Err(ConfigError::ConfigParseError(
field(),
"length must be >= min".to_string(),
));
}
}
if let Some(max) = max {
if len > max {
return Err(ConfigError::ConfigParseError(
field(),
"length must be <= max".to_string(),
));
}
}
Ok(())
}
pub fn validate_not_empty<T: ValidateLength, F: Fn() -> String>(
field: F,
value: &T,
) -> Result<(), ConfigError> {
if value.validate_len() == 0 {
return Err(ConfigError::ConfigParseError(
field(),
"value must not be empty".to_string(),
));
}
Ok(())
}
pub fn validate_custom<T, F, K>(field: K, value: &T, f: F) -> Result<(), ConfigError>
where
F: Fn(&T) -> Result<(), String>,
K: Fn() -> String,
{
if let Err(err) = f(value) {
return Err(ConfigError::ConfigParseError(field(), err));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn key(name: &'static str) -> impl Fn() -> String {
move || name.to_string()
}
#[test]
fn validate_range_numeric_and_duration() {
assert!(validate_range(key("u8"), &5u8, Some(&1u8), Some(&10u8)).is_ok());
assert!(validate_range(key("u8"), &0u8, Some(&1u8), None).is_err());
assert!(validate_range(key("u16"), &5u16, Some(&1u16), Some(&10u16)).is_ok());
assert!(validate_range(key("u32"), &5u32, Some(&1u32), Some(&10u32)).is_ok());
assert!(validate_range(key("u64"), &5u64, Some(&1u64), Some(&10u64)).is_ok());
assert!(validate_range(key("u128"), &5u128, Some(&1u128), Some(&10u128)).is_ok());
assert!(validate_range(key("usize"), &5usize, Some(&1usize), Some(&10usize)).is_ok());
assert!(validate_range(key("i8"), &-2i8, Some(&-5i8), Some(&-1i8)).is_ok());
assert!(validate_range(key("i16"), &-2i16, Some(&-5i16), Some(&-1i16)).is_ok());
assert!(validate_range(key("i32"), &-2i32, Some(&-5i32), Some(&-1i32)).is_ok());
assert!(validate_range(key("i64"), &-2i64, Some(&-5i64), Some(&-1i64)).is_ok());
assert!(validate_range(key("i128"), &-2i128, Some(&-5i128), Some(&-1i128)).is_ok());
assert!(validate_range(key("isize"), &-2isize, Some(&-5isize), Some(&-1isize)).is_ok());
assert!(validate_range(key("f32"), &1.5f32, Some(&0.5f32), Some(&2.0f32)).is_ok());
assert!(validate_range(key("f64"), &1.5f64, Some(&0.5f64), Some(&2.0f64)).is_ok());
let dur = Duration::from_secs(5);
assert!(validate_range(key("dur"), &dur, Some(&Duration::from_secs(1)), None).is_ok());
assert!(validate_range(key("dur"), &dur, Some(&Duration::from_secs(6)), None).is_err());
}
#[test]
fn validate_range_rejects_nan() {
let value = f64::NAN;
let err = validate_range(key("nan"), &value, Some(&0.0f64), Some(&1.0f64))
.expect_err("expected NaN to be rejected");
match err {
ConfigError::ConfigParseError(key, message) => {
assert_eq!(key, "nan");
assert!(message.contains("not comparable"));
}
other => panic!("unexpected error: {:?}", other),
}
}
#[test]
fn validate_length_supported_types() {
let s = "hello".to_string();
assert!(validate_length(key("s"), &s, Some(1), Some(10)).is_ok());
assert!(validate_length(key("s"), &s, Some(6), None).is_err());
let v = vec![1, 2, 3];
assert!(validate_length(key("v"), &v, Some(1), Some(3)).is_ok());
assert!(validate_length(key("v"), &v, None, Some(2)).is_err());
let mut hm = HashMap::new();
hm.insert("a", 1);
assert!(validate_length(key("hm"), &hm, Some(1), None).is_ok());
let mut bm = BTreeMap::new();
bm.insert("a", 1);
bm.insert("b", 2);
assert!(validate_length(key("bm"), &bm, Some(1), Some(2)).is_ok());
let os = OsString::from("abc");
assert!(validate_length(key("os"), &os, Some(1), Some(3)).is_ok());
let pb = PathBuf::from("/tmp");
assert!(validate_length(key("pb"), &pb, Some(1), None).is_ok());
}
#[test]
fn validate_not_empty_supported_types() {
let s = "hello".to_string();
assert!(validate_not_empty(key("s"), &s).is_ok());
let s_empty = "".to_string();
assert!(validate_not_empty(key("s"), &s_empty).is_err());
let v = vec![1, 2, 3];
assert!(validate_not_empty(key("v"), &v).is_ok());
let v_empty: Vec<u8> = Vec::new();
assert!(validate_not_empty(key("v"), &v_empty).is_err());
let mut hm = HashMap::new();
hm.insert("a", 1);
assert!(validate_not_empty(key("hm"), &hm).is_ok());
let hm_empty: HashMap<&str, u8> = HashMap::new();
assert!(validate_not_empty(key("hm"), &hm_empty).is_err());
let os = OsString::from("abc");
assert!(validate_not_empty(key("os"), &os).is_ok());
let os_empty = OsString::from("");
assert!(validate_not_empty(key("os"), &os_empty).is_err());
let pb = PathBuf::from("/tmp");
assert!(validate_not_empty(key("pb"), &pb).is_ok());
let pb_empty = PathBuf::from("");
assert!(validate_not_empty(key("pb"), &pb_empty).is_err());
}
}