formbeam 0.0.5

Form system for the Hornbeam template engine (derive macros)
Documentation
use std::{collections::BTreeMap, convert::Infallible, sync::LazyLock};

use async_trait::async_trait;
pub use regex::Regex;

use crate::{
    errors::{FieldError, FieldErrors, FieldUnit},
    traits::FieldValidator,
};

/// Constrains the minimum and maximum length, counted in `char`s, of a text input.
pub struct LengthInChars {
    pub min_length: u32,
    pub max_length: u32,
}

#[async_trait]
impl FieldValidator<()> for LengthInChars {
    type Error = Infallible;

    async fn validate(
        &self,
        value: &str,
        errors: &mut FieldErrors,
        _context: &mut (),
    ) -> Result<(), Self::Error> {
        // Clamp to u32::MAX
        let length = value.chars().count().min(u32::MAX as usize) as u32;

        if length < self.min_length {
            errors.push(FieldError::TooShort {
                current: length,
                min_length: self.min_length,
                max_length: self.max_length,
                unit: FieldUnit::Characters,
            });
        }

        if length > self.max_length {
            errors.push(FieldError::TooLong {
                current: length,
                min_length: self.min_length,
                max_length: self.max_length,
                unit: FieldUnit::Characters,
            });
        }
        Ok(())
    }
}

/// Regex for an e-mail address according to
/// <https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address>
///
/// This is likely representative of what browsers accept, but note that
/// it intentionally deviates from RFC 5322 which is not considered practical.
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("email regex")
});

pub struct Email;

#[async_trait]
impl FieldValidator<()> for Email {
    type Error = Infallible;

    async fn validate(
        &self,
        value: &str,
        errors: &mut FieldErrors,
        _context: &mut (),
    ) -> Result<(), Self::Error> {
        if !EMAIL_REGEX.is_match_at(value, 0) {
            errors.push(FieldError::Custom {
                code: "invalid_email".to_owned(),
                description: "Not a valid e-mail address".to_owned(),
                values: BTreeMap::new(),
            })
        }
        Ok(())
    }
}

#[async_trait]
impl FieldValidator<()> for Regex {
    type Error = Infallible;

    async fn validate(
        &self,
        value: &str,
        errors: &mut FieldErrors,
        _context: &mut (),
    ) -> Result<(), Self::Error> {
        if !self.is_match_at(value, 0) {
            errors.push(FieldError::Custom {
                code: "regex_unmatched".to_owned(),
                description: "Field did not match the intended pattern".to_owned(),
                values: BTreeMap::new(),
            })
        }
        Ok(())
    }
}