use crate::forms::base::*;
use crate::utils::aliases::ATera;
use crate::utils::trad::{t, tf};
use async_trait::async_trait;
use serde::Serialize;
use serde_json::json;
#[derive(Clone, Serialize, Debug)]
pub struct NumericField {
pub base: FieldConfig,
pub config: NumericConfig,
pub min_digits: Option<usize>,
pub max_digits: Option<usize>,
#[serde(skip)]
pub int_range: Option<(i128, i128)>,
}
impl CommonFieldConfig for NumericField {
fn get_field_config(&self) -> &FieldConfig {
&self.base
}
fn get_field_config_mut(&mut self) -> &mut FieldConfig {
&mut self.base
}
}
impl NumericField {
fn create(name: &str, type_field: &str, config: NumericConfig) -> Self {
Self {
base: FieldConfig::new(name, type_field, "base_number.html"),
config,
min_digits: None,
max_digits: None,
int_range: None,
}
}
pub fn digits(mut self, min: usize, max: usize) -> Self {
self.min_digits = Some(min);
self.max_digits = Some(max);
self
}
pub fn integer(name: &str) -> Self {
Self::create(
name,
"number",
NumericConfig::Integer {
min: None,
max: None,
},
)
}
pub fn integer_in(name: &str, min: i128, max: i128) -> Self {
let mut field = Self::integer(name);
field.int_range = Some((min, max));
field
}
pub fn required(mut self) -> Self {
self.set_required(true, None);
self
}
pub fn placeholder(mut self, p: &str) -> Self {
self.set_placeholder(p);
self
}
pub fn float(name: &str) -> Self {
Self::create(name, "number", NumericConfig::Float { value: None })
}
pub fn decimal(name: &str) -> Self {
Self::create(name, "number", NumericConfig::Decimal { value: None })
}
pub fn percent(name: &str) -> Self {
Self::create(
name,
"number",
NumericConfig::Percent {
value: Range {
min: 0.0,
max: 100.0,
},
},
)
}
pub fn range(name: &str, min: f64, max: f64, default: f64) -> Self {
let mut field = Self::create(
name,
"range",
NumericConfig::Range {
value: Range { min, max },
default,
step: 1.0,
},
);
field.base.value = default.to_string();
field
}
pub fn min(mut self, val: f64, msg: &str) -> Self {
match &mut self.config {
NumericConfig::Integer { min, .. } => *min = Some(val as i64),
NumericConfig::Float { value } | NumericConfig::Decimal { value, .. } => {
if let Some(f) = value {
f.min = val;
} else {
*value = Some(Range {
min: val,
max: f64::MAX,
});
}
}
NumericConfig::Percent { value } | NumericConfig::Range { value, .. } => {
value.min = val;
}
}
if !msg.is_empty() {
self.base
.extra_context
.insert("min_message".to_string(), json!(msg));
}
self
}
pub fn max(mut self, val: f64, msg: &str) -> Self {
match &mut self.config {
NumericConfig::Integer { max, .. } => *max = Some(val as i64),
NumericConfig::Float { value } | NumericConfig::Decimal { value, .. } => {
if let Some(f) = value {
f.max = val;
} else {
*value = Some(Range {
min: f64::MIN,
max: val,
});
}
}
NumericConfig::Percent { value } | NumericConfig::Range { value, .. } => {
value.max = val;
}
}
if !msg.is_empty() {
self.base
.extra_context
.insert("max_message".to_string(), json!(msg));
}
self
}
pub fn step(mut self, s: f64) -> Self {
if let NumericConfig::Range { step, .. } = &mut self.config {
*step = s;
}
self
}
pub fn label(mut self, label: &str) -> Self {
self.base.label = label.to_string();
self
}
}
impl NumericField {
fn bound_message(&self, key: &str, default: impl FnOnce() -> String) -> String {
self.base
.extra_context
.get(key)
.and_then(|v| v.as_str())
.map_or_else(default, str::to_string)
}
}
#[async_trait]
impl FormField for NumericField {
fn bounds(&self) -> crate::forms::base::FieldBounds {
use crate::forms::base::FieldBounds;
match &self.config {
NumericConfig::Integer { min, max } => FieldBounds {
min_int: *min,
max_int: *max,
..Default::default()
},
NumericConfig::Float { value } | NumericConfig::Decimal { value } => FieldBounds {
min_float: value.as_ref().map(|r| r.min),
max_float: value.as_ref().map(|r| r.max),
..Default::default()
},
NumericConfig::Percent { value } | NumericConfig::Range { value, .. } => FieldBounds {
min_float: Some(value.min),
max_float: Some(value.max),
..Default::default()
},
}
}
fn set_type_bounds(&mut self, min: i128, max: i128) -> bool {
if !matches!(self.config, NumericConfig::Integer { .. }) {
return false;
}
self.int_range = Some((min, max));
true
}
async fn validate(&mut self) -> bool {
let val = self.base.value.trim();
if self.base.is_required.choice && val.is_empty() {
self.set_error(
self.base
.is_required
.message
.clone()
.unwrap_or_else(|| t("forms.number_required").to_string()),
);
return false;
}
if val.is_empty() {
return true;
}
let normalized = val.replace(',', ".");
let current_digits = normalized
.find('.')
.map(|dot| normalized[dot.saturating_add(1)..].len())
.unwrap_or(0);
if current_digits < self.min_digits.unwrap_or(0) {
self.set_error(tf("forms.precision_min", &[&self.min_digits.unwrap_or(0)]));
return false;
}
if current_digits > self.max_digits.unwrap_or(usize::MAX) {
self.set_error(tf(
"forms.precision_max",
&[&self.max_digits.unwrap_or(usize::MAX)],
));
return false;
}
let canonical = match &self.config {
NumericConfig::Integer { min, max } => {
let Ok(v) = normalized.parse::<i128>() else {
self.set_error(t("forms.integer_required").to_string());
return false;
};
let (lo, hi) = self.int_range.unwrap_or((i64::MIN.into(), i64::MAX.into()));
let lo = min.map_or(lo, |m| lo.max(m.into()));
let hi = max.map_or(hi, |m| hi.min(m.into()));
if v < lo {
let msg = self.bound_message("min_message", || tf("forms.min_value", &[&lo]));
self.set_error(msg);
return false;
}
if v > hi {
let msg = self.bound_message("max_message", || tf("forms.max_value", &[&hi]));
self.set_error(msg);
return false;
}
v.to_string()
}
NumericConfig::Decimal { value, .. } | NumericConfig::Float { value } => {
let parsed = if matches!(self.config, NumericConfig::Decimal { .. }) {
rust_decimal::Decimal::from_str_exact(&normalized)
.ok()
.and_then(|d| rust_decimal::prelude::ToPrimitive::to_f64(&d))
} else {
normalized.parse::<f64>().ok().filter(|v| v.is_finite())
};
let Some(v) = parsed else {
self.set_error(t("forms.number_invalid").to_string());
return false;
};
if let Some(f) = value.as_ref() {
let (min, max) = (f.min, f.max);
if v < min {
let msg =
self.bound_message("min_message", || tf("forms.min_value", &[&min]));
self.set_error(msg);
return false;
}
if v > max {
let msg =
self.bound_message("max_message", || tf("forms.max_value", &[&max]));
self.set_error(msg);
return false;
}
}
normalized
}
NumericConfig::Percent { value } | NumericConfig::Range { value, .. } => {
let (min, max) = (value.min, value.max);
match normalized.parse::<f64>() {
Ok(v) if v >= min && v <= max => normalized,
Ok(v) if v.is_finite() => {
let key = if v < min {
"min_message"
} else {
"max_message"
};
let msg = self.bound_message(key, || t("forms.number_invalid").to_string());
self.set_error(msg);
return false;
}
_ => {
self.set_error(t("forms.number_invalid").to_string());
return false;
}
}
}
};
self.base.value = canonical;
self.clear_error();
true
}
fn render(&self, tera: &ATera) -> Result<String, String> {
let mut context = self.base_context();
context.insert("config", &self.config);
tera.render(&self.base.template_name, &context)
.map_err(|e| {
tf(
"forms.finalize_error",
&[&self.base.template_name, &e.to_string()],
)
.to_string()
})
}
}