runique 3.0.2

A Django-inspired web framework for Rust with ORM, templates, and comprehensive security middleware
Documentation
//! Selection field: `ChoiceField` with groups, multi-select, and dynamic options.
use crate::forms::base::{CommonFieldConfig, FieldConfig, FormField};
use crate::utils::aliases::ATera;
use crate::utils::trad::{t, tf};
use async_trait::async_trait;
use serde::Serialize;
use serde_json::{Value, json};

/// Option for choice fields
#[derive(Clone, Debug, Serialize)]
pub struct ChoiceOption {
    pub value: String,
    pub label: String,
    pub selected: bool,
}

impl ChoiceOption {
    /// Creates a new option with the given submitted value and display label.
    pub fn new(value: &str, label: &str) -> Self {
        Self {
            value: value.to_string(),
            label: label.to_string(),
            selected: false,
        }
    }

    /// Marks this option as pre-selected.
    pub fn selected(mut self) -> Self {
        self.selected = true;
        self
    }
}

/// Dropdown `<select>` field. Use [`ChoiceField::multiple`] for multi-select.
#[derive(Clone, Serialize, Debug)]
pub struct ChoiceField {
    pub base: FieldConfig,
    pub choices: Vec<ChoiceOption>,
    pub multiple: bool,
}

impl ChoiceField {
    /// Creates an empty dropdown. Add options with [`choices`](ChoiceField::choices) or [`add_choice`](ChoiceField::add_choice).
    pub fn new(name: &str) -> Self {
        Self {
            base: FieldConfig::new(name, "select", "base_select.html"),
            choices: Vec::new(),
            multiple: false,
        }
    }

    /// Enables `<select multiple>`. Submitted value is a comma-separated string.
    pub fn multiple(mut self) -> Self {
        self.multiple = true;
        self.base.type_field = "select-multiple".to_string();
        self
    }

    /// Replaces all options at once.
    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
        self.choices = choices;
        self
    }

    /// Appends a single option.
    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
        self.choices.push(ChoiceOption::new(value, label));
        self
    }

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

    /// Marks the field as required (empty selection fails validation).
    pub fn required(mut self) -> Self {
        self.set_required(true, None);
        self
    }
}

impl CommonFieldConfig for ChoiceField {
    fn get_field_config(&self) -> &FieldConfig {
        &self.base
    }

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

#[async_trait]
impl FormField for ChoiceField {
    /// A `multiple()` select sends one value per selected option, which reach
    /// the field joined as `"a,b"`: each one is marked selected for re-display.
    fn set_value(&mut self, value: &str) {
        self.base.value = value.to_string();
        if self.multiple {
            let selected: Vec<&str> = value.split(',').map(str::trim).collect();
            for choice in &mut self.choices {
                choice.selected = selected.contains(&choice.value.as_str());
            }
        }
    }

    async fn validate(&mut self) -> bool {
        let val = self.base.value.trim();

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

        if !val.is_empty() && self.multiple {
            // Every selected value must be one of the choices, not the joined string.
            for selected in val.split(',').map(str::trim) {
                if !self.choices.iter().any(|c| c.value == selected) {
                    self.set_error(tf("forms.choice_invalid_value", &[selected]));
                    return false;
                }
            }
        } else if !val.is_empty() {
            // Check that the value exists in the choices
            let valid = self.choices.iter().any(|c| c.value == val);
            if !valid {
                self.set_error(t("forms.choice_invalid").to_string());
                return false;
            }
        }

        self.clear_error();
        true
    }

    fn render(&self, tera: &ATera) -> Result<String, String> {
        let mut context = self.base_context();
        context.insert("choices", &self.choices);
        context.insert("multiple", &self.multiple);

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

/// A group of `<input type="radio">` buttons where one option can be selected.
#[derive(Clone, Serialize, Debug)]
pub struct RadioField {
    pub base: FieldConfig,
    pub choices: Vec<ChoiceOption>,
}

impl RadioField {
    /// Creates an empty radio group. Add options with [`choices`](RadioField::choices) or [`add_choice`](RadioField::add_choice).
    pub fn new(name: &str) -> Self {
        Self {
            base: FieldConfig::new(name, "radio", "base_radio.html"),
            choices: Vec::new(),
        }
    }

    /// Replaces all options at once.
    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
        self.choices = choices;
        self
    }

    /// Appends a single option.
    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
        self.choices.push(ChoiceOption::new(value, label));
        self
    }

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

    /// Marks the field as required (no selection fails validation).
    pub fn required(mut self) -> Self {
        self.set_required(true, None);
        self
    }
}

impl CommonFieldConfig for RadioField {
    fn get_field_config(&self) -> &FieldConfig {
        &self.base
    }

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

#[async_trait]
impl FormField for RadioField {
    async fn validate(&mut self) -> bool {
        let val = self.base.value.trim();

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

        if !val.is_empty() {
            let valid = self.choices.iter().any(|c| c.value == val);
            if !valid {
                self.set_error(t("forms.choice_invalid").to_string());
                return false;
            }
        }

        self.clear_error();
        true
    }

    fn render(&self, tera: &ATera) -> Result<String, String> {
        let mut context = self.base_context();
        context.insert("choices", &self.choices);
        context.insert("meta", &self.to_json_meta());
        tera.render(&self.base.template_name, &context)
            .map_err(|e| {
                tf(
                    "forms.finalize_error",
                    &[&self.base.template_name, &e.to_string()],
                )
                .to_string()
            })
    }

    fn to_json_value(&self) -> Value {
        json!(self.base.value)
    }

    fn to_json_required(&self) -> Value {
        json!(self.base.is_required)
    }

    fn to_json_attributes(&self) -> Value {
        json!(self.base.html_attributes)
    }
}

/// A group of checkboxes where multiple options can be selected simultaneously. Submitted value is a comma-separated string.
#[derive(Clone, Serialize, Debug)]
pub struct CheckboxField {
    pub base: FieldConfig,
    pub choices: Vec<ChoiceOption>,
}

impl CheckboxField {
    /// Creates an empty checkbox group. Add options with [`choices`](CheckboxField::choices) or [`add_choice`](CheckboxField::add_choice).
    pub fn new(name: &str) -> Self {
        Self {
            base: FieldConfig::new(name, "checkbox", "base_checkbox.html"),
            choices: Vec::new(),
        }
    }

    /// Replaces all options at once.
    pub fn choices(mut self, choices: Vec<ChoiceOption>) -> Self {
        self.choices = choices;
        self
    }

    /// Appends a single option.
    pub fn add_choice(mut self, value: &str, label: &str) -> Self {
        self.choices.push(ChoiceOption::new(value, label));
        self
    }

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

    /// Marks the field as required (no selection fails validation).
    pub fn required(mut self) -> Self {
        self.set_required(true, None);
        self
    }
}
impl CommonFieldConfig for CheckboxField {
    fn get_field_config(&self) -> &FieldConfig {
        &self.base
    }

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

#[async_trait]
impl FormField for CheckboxField {
    fn set_value(&mut self, value: &str) {
        // Expected format: "value1,value2,value3"
        self.base.value = value.to_string();
        let selected_values: Vec<&str> = value.split(',').map(|s| s.trim()).collect();

        for choice in &mut self.choices {
            choice.selected = selected_values.contains(&choice.value.as_str());
        }
    }

    async fn validate(&mut self) -> bool {
        let val = self.base.value.trim();

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

        if !val.is_empty() {
            // Check that all selected values exist
            let selected_values: Vec<&str> = val.split(',').map(|s| s.trim()).collect();
            for sel_val in selected_values {
                if !self.choices.iter().any(|c| c.value == sel_val) {
                    self.set_error(tf("forms.choice_invalid_value", &[sel_val]));
                    return false;
                }
            }
        }

        self.set_error("".into());
        true
    }

    fn render(&self, tera: &ATera) -> Result<String, String> {
        let mut context = self.base_context();
        context.insert("choices", &self.choices);
        context.insert("meta", &self.to_json_meta());

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