valida 1.1.2

Modular validation system for Rust with support for nested structures and localization.
Documentation
use crate::core::contract::IValidatorRule;
use crate::core::errors::ValidationError;
use crate::core::rules::str_ref::StrAsRef;
use std::collections::HashMap;

#[derive(Debug, PartialEq, PartialOrd, Eq, Clone, Copy)]
pub enum StrengthLevel {
    VeryWeak = 0,
    Weak = 1,
    Medium = 2,
    Strong = 3,
    VeryStrong = 4,
}

pub fn estimate_password_strength(password: &str) -> StrengthLevel {
    let length = password.len();
    if length == 0 {
        return StrengthLevel::VeryWeak;
    }

    let mut control = 0;
    let mut digit = 0;
    let mut upper = 0;
    let mut lower = 0;
    let mut symbol = 0;
    let mut other = 0;

    let mut unique = std::collections::HashSet::new();

    for chr in password.chars() {
        unique.insert(chr);
        let code = chr as u32;
        match code {
            0..=31 | 127 => control = 33,
            48..=57 => digit = 10,
            65..=90 => upper = 26,
            97..=122 => lower = 26,
            128.. => other = 128,
            _ => symbol = 33,
        }
    }

    let pool: f64 = (lower + upper + digit + symbol + control + other) as f64;
    let chars = unique.len();
    let entropy = (chars as f64) * pool.log2() + ((length - chars) as f64) * (chars as f64).log2();

    match entropy {
        e if e >= 120.0 => StrengthLevel::VeryStrong,
        e if e >= 100.0 => StrengthLevel::Strong,
        e if e >= 80.0 => StrengthLevel::Medium,
        e if e >= 60.0 => StrengthLevel::Weak,
        _ => StrengthLevel::VeryWeak,
    }
}

pub struct PasswordStrength {
    pub level: StrengthLevel,
}

impl<T: StrAsRef> IValidatorRule<T> for PasswordStrength {
    fn validate(&self, value: &T) -> Result<(), ValidationError> {
        if let Some(password) = value.as_str_ref() {
            let strength = estimate_password_strength(password);
            if strength < self.level {
                return Err(ValidationError::new_with_params(
                    "validator.password_strength",
                    HashMap::from([("strength".into(), format!("{strength:?}"))]),
                ));
            }
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::contract::IValidatorRule;

    fn validator(level: StrengthLevel) -> PasswordStrength {
        PasswordStrength { level }
    }

    #[test]
    fn passes_strong_password() {
        let value = "Rust4Life!Secure2024";
        assert!(validator(StrengthLevel::Strong).validate(&value).is_ok());
    }

    #[test]
    fn fails_weak_password() {
        let value = "abc123";
        assert!(validator(StrengthLevel::Medium).validate(&value).is_err());
    }

    #[test]
    fn validates_empty_string_as_very_weak() {
        let value = "";
        assert!(validator(StrengthLevel::VeryWeak).validate(&value).is_ok());
    }

    #[test]
    fn validates_option_none() {
        let value: Option<String> = None;
        assert!(validator(StrengthLevel::Medium).validate(&value).is_ok());
    }
}