runique 2.1.1-alpha.2

A Django-inspired web framework for Rust with ORM, templates, and comprehensive security middleware
Documentation
//! Champs texte : `TextField`, `EmailField`, `UrlField`, `TextAreaField`, `PasswordField`.
pub use crate::forms::generic::GenericField;
use crate::forms::{
    base::{CommonFieldConfig, FieldConfig, FormField, TextConfig},
    options::LengthConstraint,
};
use crate::utils::{
    password::{PasswordConfig, PasswordService},
    trad::{t, tf},
};
use serde::Serialize;
use std::sync::Arc;
use tera::{Context, Tera};
use validator::{ValidateEmail, ValidateUrl};

// Main structure for text fields
#[derive(Clone, Serialize, Debug)]
pub struct TextField {
    pub base: FieldConfig,
    pub config: TextConfig,
    pub format: SpecialFormat,
    pub hash_password: bool, // true by default
}

// Special formats for text fields
#[derive(Clone, Debug, Serialize, PartialEq)]
pub enum SpecialFormat {
    None,
    Email,
    Url,
    Password,
    RichText,
    Csrf,
}
impl CommonFieldConfig for TextField {
    fn get_field_config(&self) -> &FieldConfig {
        &self.base
    }

    fn get_field_config_mut(&mut self) -> &mut FieldConfig {
        &mut self.base
    }
}

// Implementation of methods for TextField
impl TextField {
    // Private generic constructor based on the generic field — avoids code duplication
    fn create(name: &str, type_field: &str, format: SpecialFormat) -> Self {
        Self {
            base: FieldConfig::new(name, type_field, "base_string"),
            config: TextConfig::default(),
            format,
            hash_password: true,
        }
    }
    /// Disables automatic password hashing (helper)
    pub fn no_hash(mut self) -> Self {
        self.hash_password = false;
        self
    }
    pub fn min_length(mut self, min: u32, msg: &str) -> Self {
        self.config.min_length = Some(LengthConstraint {
            value: min,
            message: (!msg.is_empty()).then(|| msg.to_string()),
        });
        self
    }

    pub fn max_length(mut self, max: u32, msg: &str) -> Self {
        self.config.max_length = Some(LengthConstraint {
            value: max,
            message: (!msg.is_empty()).then(|| msg.to_string()),
        });
        self
    }

    // Public constructors for different text field types
    pub fn text(name: &str) -> Self {
        Self::create(name, "text", SpecialFormat::None)
    }
    pub fn textarea(name: &str) -> Self {
        Self::create(name, "textarea", SpecialFormat::None)
    }
    pub fn richtext(name: &str) -> Self {
        Self::create(name, "richtext", SpecialFormat::RichText)
    }
    pub fn password(name: &str) -> Self {
        Self::create(name, "password", SpecialFormat::Password)
    }
    pub fn email(name: &str) -> Self {
        let mut field = Self::create(name, "email", SpecialFormat::Email);
        field.base.value = field.base.value.to_lowercase();
        field
    }
    pub fn url(name: &str) -> Self {
        Self::create(name, "url", SpecialFormat::Url)
    }

    // Builder methods
    pub fn required(mut self) -> Self {
        self.set_required(true, None);
        self
    }

    pub fn label(mut self, label: &str) -> Self {
        self.base.label = label.to_string();
        self
    }

    pub fn placeholder(mut self, p: &str) -> Self {
        self.set_placeholder(p);
        self
    }

    pub fn readonly(mut self, msg: &str) -> Self {
        self.set_readonly(true, Some(msg));
        self
    }

    pub fn disabled(mut self, msg: &str) -> Self {
        self.set_disabled(true, Some(msg));
        self
    }

    pub fn rows(mut self, rows: usize) -> Self {
        self.base
            .extra_context
            .insert("rows".to_string(), serde_json::json!(rows));
        self
    }
}

impl FormField for TextField {
    fn set_value(&mut self, value: &str) {
        let cleaned = match self.format {
            SpecialFormat::Password | SpecialFormat::Csrf => value.to_string(),
            SpecialFormat::RichText => crate::utils::sanitizer::sanitize_rich(value),
            _ => {
                if value.contains('<') || value.contains('>') {
                    value.replace(['<', '>', '&'], "").trim().to_string()
                } else {
                    crate::utils::sanitizer::sanitize_strict(value)
                }
            }
        };
        self.base.value = cleaned;
    }
    fn validate(&mut self) -> bool {
        // Initial trim
        let mut val = self.base.value.trim().to_string();

        // Required field validation
        if self.base.is_required.choice && val.is_empty() {
            let msg = self
                .base
                .is_required
                .message
                .clone()
                .unwrap_or_else(|| t("forms.required").to_string());
            self.set_error(msg);
            return false;
        }

        if val.is_empty() {
            return true;
        }

        // Minimum length validation
        if let Some(limits) = &self.config.min_length {
            let count: u32 = val.chars().count() as u32;
            if count < limits.value {
                let msg = limits
                    .message
                    .clone()
                    .unwrap_or_else(|| tf("forms.too_short", &[&limits.value]));
                self.set_error(msg);
                return false;
            }
        }

        // Maximum length validation
        if let Some(limits) = &self.config.max_length {
            let count: u32 = val.chars().count() as u32;
            if count > limits.value {
                let msg = limits
                    .message
                    .clone()
                    .unwrap_or_else(|| tf("forms.too_long", &[&limits.value]));
                self.set_error(msg);
                return false;
            }
        }

        // Special format validation
        match &self.format {
            SpecialFormat::Email if !val.validate_email() => {
                self.set_error(t("forms.email_invalid").to_string());
                return false;
            }
            SpecialFormat::Email => {
                val = val.to_lowercase();
            }
            SpecialFormat::Url if !val.validate_url() => {
                self.set_error(t("forms.url_invalid").to_string());
                return false;
            }
            _ => {}
        }

        // Update the cleaned value
        self.base.value = val;

        self.clear_error();
        true
    }

    fn finalize(&mut self) -> Result<(), String> {
        if let SpecialFormat::Password = &self.format
            && self.hash_password
            && !self.base.value.is_empty()
        {
            let config = crate::utils::password::password_get();
            if let PasswordConfig::Auto(_) = config {
                let service = PasswordService::new(config);
                if !service.is_already_hashed(&self.base.value) {
                    self.base.value = service
                        .hash(&self.base.value)
                        .map_err(|e| tf("forms.hash_error", &[&e.to_string()]).to_string())?;
                }
            }
        }
        Ok(())
    }
    fn render(&self, tera: &Arc<Tera>) -> Result<String, String> {
        let mut context = Context::new();

        // Prepare a "secured" version of the base
        let mut base_data = self.base.clone();
        if let SpecialFormat::Password = &self.format {
            base_data.value = "".to_string();
        }

        context.insert("field", &base_data);
        context.insert("input_type", &self.base.type_field);

        // IMPORTANT: Inject readonly/disabled config
        context.insert("readonly", &self.to_json_readonly());
        context.insert("disabled", &self.to_json_disabled());

        if let Some(l) = &self.config.min_length {
            context.insert("min_length", &l.value);
        }
        if let Some(l) = &self.config.max_length {
            context.insert("max_length", &l.value);
        }

        if let Some(rows) = self.base.extra_context.get("rows").and_then(|r| r.as_u64()) {
            context.insert("rows", &rows);
        }

        tera.render(&self.base.template_name, &context)
            .map_err(|e| {
                tf(
                    "forms.finalize_error",
                    &[&self.base.template_name, &e.to_string()],
                )
                .to_string()
            })
    }
}