pub use crate::forms::{
base::FormField, form::Forms, renderer::FormRenderer, validator::ValidationError,
};
use crate::utils::{
aliases::{ATera, StrMap},
trad::t,
};
use async_trait::async_trait;
use axum::http::Method;
use sea_orm::{DatabaseConnection, DatabaseTransaction, DbErr, TransactionTrait};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaveContext {
Create,
Update,
Delete,
}
dyn_clone::clone_trait_object!(FormField);
fn cleaned_value(form: &Forms, name: &str) -> Option<String> {
if !form.fields.contains_key(name) {
return None;
}
if let Some(val) = form.fields.get(name).map(|f| f.value())
&& !val.is_empty()
{
return Some(val.to_string().trim().to_string());
}
form.path_params
.get(name)
.or_else(|| form.query_params.get(name))
.map(|s| s.to_string())
}
#[doc = include_str!("../../doc-tests/form/form_proc_macro.md")]
#[doc = include_str!("../../doc-tests/form/form_clean_save.md")]
#[async_trait]
pub trait RuniqueForm: Sized + Send + Sync {
fn register_fields(form: &mut Forms);
fn from_form(form: Forms) -> Self;
fn get_form(&self) -> &Forms;
fn get_form_mut(&mut self) -> &mut Forms;
fn cleaned_string(&self, name: &str) -> Option<String> {
cleaned_value(self.get_form(), name)
}
fn cleaned_i32(&self, name: &str) -> Option<i32> {
cleaned_value(self.get_form(), name)?.parse().ok()
}
fn cleaned_i64(&self, name: &str) -> Option<i64> {
cleaned_value(self.get_form(), name)?.parse().ok()
}
fn cleaned_u32(&self, name: &str) -> Option<u32> {
cleaned_value(self.get_form(), name)?.parse().ok()
}
fn cleaned_u64(&self, name: &str) -> Option<u64> {
cleaned_value(self.get_form(), name)?.parse().ok()
}
fn cleaned_f32(&self, name: &str) -> Option<f32> {
use rust_decimal::prelude::ToPrimitive;
let v = cleaned_value(self.get_form(), name)?.replace(',', ".");
rust_decimal::Decimal::from_str_exact(&v).ok()?.to_f32()
}
fn cleaned_f64(&self, name: &str) -> Option<f64> {
use rust_decimal::prelude::ToPrimitive;
let v = cleaned_value(self.get_form(), name)?.replace(',', ".");
rust_decimal::Decimal::from_str_exact(&v).ok()?.to_f64()
}
fn cleaned_bool(&self, name: &str) -> Option<bool> {
let v = cleaned_value(self.get_form(), name)?;
Some(matches!(v.to_lowercase().as_str(), "true" | "1" | "on"))
}
fn cleaned_uuid(&self, name: &str) -> Option<uuid::Uuid> {
uuid::Uuid::parse_str(&cleaned_value(self.get_form(), name)?).ok()
}
fn cleaned_naive_date(&self, name: &str) -> Option<chrono::NaiveDate> {
chrono::NaiveDate::parse_from_str(&cleaned_value(self.get_form(), name)?, "%Y-%m-%d").ok()
}
fn cleaned_naive_time(&self, name: &str) -> Option<chrono::NaiveTime> {
chrono::NaiveTime::parse_from_str(&cleaned_value(self.get_form(), name)?, "%H:%M").ok()
}
fn cleaned_naive_datetime(&self, name: &str) -> Option<chrono::NaiveDateTime> {
chrono::NaiveDateTime::parse_from_str(
&cleaned_value(self.get_form(), name)?,
"%Y-%m-%dT%H:%M",
)
.ok()
}
fn cleaned_datetime_utc(&self, name: &str) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::parse_from_rfc3339(&cleaned_value(self.get_form(), name)?)
.ok()
.map(|dt| dt.with_timezone(&chrono::Utc))
}
fn cleaned_enum<T: sea_orm::ActiveEnum<Value = String>>(&self, name: &str) -> Option<T> {
let v = cleaned_value(self.get_form(), name)?;
T::try_from_value(&v).ok()
}
fn add_value(&mut self, name: &str, value: &str) -> &mut Self {
self.get_form_mut().add_value(name, value);
self
}
fn label(&mut self, name: &str, label: &str) -> &mut Self {
self.get_form_mut().field_label(name, label);
self
}
fn placeholder(&mut self, name: &str, placeholder: &str) -> &mut Self {
self.get_form_mut().field_placeholder(name, placeholder);
self
}
fn required(&mut self, name: &str, required: bool) -> &mut Self {
self.get_form_mut().field_required(name, required);
self
}
fn readonly(&mut self, name: &str, readonly: bool) -> &mut Self {
self.get_form_mut().field_readonly(name, readonly);
self
}
fn disabled(&mut self, name: &str, disabled: bool) -> &mut Self {
self.get_form_mut().field_disabled(name, disabled);
self
}
fn attr(&mut self, name: &str, key: &str, value: &str) -> &mut Self {
self.get_form_mut().field_attr(name, key, value);
self
}
fn max_size(
&mut self,
name: &str,
size: crate::forms::fields::FileSize,
) -> Result<&mut Self, String> {
self.get_form_mut().field_max_size(name, size)?;
Ok(self)
}
fn clear(&mut self) {
self.get_form_mut().clear_values();
}
async fn clean_field(&mut self, name: &str) -> bool {
self.get_form().fields.contains_key(name)
}
async fn clean(&mut self) -> Result<(), StrMap> {
Ok(())
}
async fn is_valid(&mut self) -> bool {
if !self.get_form().is_submitted() {
return false;
}
let mut fields_valid = match self.get_form_mut().is_valid() {
Ok(valid) => valid,
Err(ValidationError::StackOverflow) => {
self.get_form_mut()
.errors
.push(t("forms.validation_overflow").into_owned());
return false;
}
Err(_) => return false,
};
let names: Vec<String> = self.get_form().fields.keys().cloned().collect();
for name in names {
if !self.clean_field(&name).await {
fields_valid = false;
}
}
if !fields_valid {
return false;
}
match self.clean().await {
Ok(_) => {
if let Err(e) = self.get_form_mut().finalize() {
self.get_form_mut().errors.push(e);
return false;
}
true
}
Err(business_errors) => {
let form = self.get_form_mut();
for (name, msg) in business_errors {
if let Some(field) = form.fields.get_mut(&name) {
field.set_error(msg);
} else {
form.errors.push(msg);
}
}
false
}
}
}
async fn on_save(&mut self, _txn: &DatabaseTransaction) -> Result<(), DbErr> {
Ok(())
}
async fn before_save(
&mut self,
_ctx: SaveContext,
_txn: &DatabaseTransaction,
) -> Result<(), DbErr> {
Ok(())
}
async fn after_save(
&mut self,
_ctx: SaveContext,
_txn: &DatabaseTransaction,
) -> Result<(), DbErr> {
Ok(())
}
async fn save(&mut self, db: &DatabaseConnection) -> Result<(), DbErr> {
if !self.get_form().is_save_allowed() {
return Err(DbErr::Custom(
"save() requires a successful is_valid() call — form not validated or invalid"
.to_string(),
));
}
let txn = db.begin().await?;
match self.on_save(&txn).await {
Ok(()) => {
txn.commit().await?;
Ok(())
}
Err(e) => {
let _ = txn.rollback().await;
Err(e)
}
}
}
async fn save_as(&mut self, ctx: SaveContext, db: &DatabaseConnection) -> Result<(), DbErr> {
if !self.get_form().is_save_allowed() {
return Err(DbErr::Custom(
"save_as() requires a successful is_valid() call — form not validated or invalid"
.to_string(),
));
}
let txn = db.begin().await?;
if let Err(e) = self.before_save(ctx, &txn).await {
let _ = txn.rollback().await;
return Err(e);
}
if let Err(e) = self.on_save(&txn).await {
let _ = txn.rollback().await;
return Err(e);
}
if let Err(e) = self.after_save(ctx, &txn).await {
let _ = txn.rollback().await;
return Err(e);
}
txn.commit().await
}
fn database_error(&mut self, err: &DbErr) {
self.get_form_mut().database_error(err);
}
fn build(tera: ATera, csrf_token: &str) -> Self {
let mut form = Forms::new(csrf_token);
let renderer = FormRenderer::new(tera);
form.set_renderer(renderer);
Self::register_fields(&mut form);
Self::from_form(form)
}
async fn build_with_data(
raw_data: &StrMap,
tera: ATera,
csrf_token: &str,
method: Method,
) -> Self {
let mut form = Forms::new(csrf_token);
let renderer = FormRenderer::new(tera);
form.set_renderer(renderer);
Self::register_fields(&mut form);
form.fill(raw_data, method);
Self::from_form(form)
}
}