add error handling in case of model errors

This commit is contained in:
Manuel Gugger 2022-07-26 17:47:18 +02:00
parent 3702474b41
commit 130a2b4098
6 changed files with 102 additions and 24 deletions

View File

@ -12,7 +12,8 @@ use struct_fields::{
get_field_for_primary_key,
get_primary_key_field_name,
get_actix_admin_fields_select_list,
get_actix_admin_fields_is_option_list
get_actix_admin_fields_is_option_list,
get_fields_for_validate_model
};
mod selectlist_fields;
@ -42,6 +43,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
let fields_for_edit_model = get_fields_for_edit_model(&fields);
let fields_for_from_model = get_fields_for_from_model(&fields);
let field_for_primary_key = get_field_for_primary_key(&fields);
let fields_for_validate_model = get_fields_for_validate_model(&fields);
let select_lists = get_select_lists(&fields);
@ -75,7 +77,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#field_for_primary_key,
values: hashmap![
#(#fields_for_from_model),*
]
],
errors: Vec::new()
}
}
}
@ -99,8 +102,13 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
}
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel {
let new_model = ActiveModel::from(model.clone());
let insert_operation = Entity::insert(new_model).exec(db).await;
let mut validation_errs = Entity::validate_model(&model);
model.errors.append(&mut validation_errs);
if !model.has_errors() {
let new_model = ActiveModel::from(model.clone());
let insert_operation = Entity::insert(new_model).exec(db).await;
}
model
}
@ -114,13 +122,17 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
}
async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> ActixAdminModel {
// TODO: separate primary key from other keys
let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap();
let mut entity: ActiveModel = entity.unwrap().into();
let mut validation_errs = Entity::validate_model(&model);
model.errors.append(&mut validation_errs);
#(#fields_for_edit_model);*;
let entity: Model = entity.update(db).await.unwrap();
if !model.has_errors() {
let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap();
let mut entity: ActiveModel = entity.unwrap().into();
#(#fields_for_edit_model);*;
let entity: Model = entity.update(db).await.unwrap();
}
model
}
@ -169,6 +181,17 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
(num_pages, model_entities)
}
fn validate_model(model: &ActixAdminModel) -> Vec<ActixAdminError> {
let mut errors = Vec::<ActixAdminError>::new();
#(#fields_for_validate_model);*;
let mut custom_errors = Entity.validate();
errors.append(&mut custom_errors);
errors
}
fn get_fields() -> Vec<ActixAdminViewModelField> {
let mut vec = Vec::new();

View File

@ -208,6 +208,32 @@ pub fn get_fields_for_from_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
.collect::<Vec<_>>()
}
pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr
.filter(|model_field| !model_field.primary_key)
.map(|model_field| {
let ident_name = model_field.ident.to_string();
let ty = model_field.ty.to_owned();
match model_field.is_option() {
true => {
let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! {
model.get_value::<#inner_ty>(#ident_name).map_err(|err| errors.push(err)).ok();
}
},
false => {
quote! {
model.get_value::<#ty>(#ident_name).map_err(|err| errors.push(err)).ok();
}
}
}
})
.collect::<Vec<_>>()
}
pub fn get_fields_for_create_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
@ -222,12 +248,12 @@ pub fn get_fields_for_create_model(fields: &Vec<ModelField>) -> Vec<TokenStream>
true => {
let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! {
#ident: Set(model.get_value::<#inner_ty>(#ident_name))
#ident: Set(model.get_value::<#inner_ty>(#ident_name).unwrap())
}
},
false => {
quote! {
#ident: Set(model.get_value::<#ty>(#ident_name).unwrap())
#ident: Set(model.get_value::<#ty>(#ident_name).unwrap().unwrap())
}
}
}
@ -249,12 +275,12 @@ pub fn get_fields_for_edit_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
true => {
let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! {
entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name))
entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name).unwrap())
}
},
false => {
quote! {
entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap())
entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap().unwrap())
}
}
}

View File

@ -2,6 +2,7 @@ use lazy_static::lazy_static;
use sea_orm::DatabaseConnection;
use std::collections::HashMap;
use tera::{Tera};
use serde::{Serialize};
pub mod view_model;
pub mod model;
@ -13,7 +14,7 @@ pub mod prelude {
pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField};
pub use actix_admin_macros::{ DeriveActixAdminModel, DeriveActixAdminSelectList };
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin};
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin, ActixAdminError };
pub use crate::{ hashmap, ActixAdminSelectListTrait };
}
@ -34,6 +35,12 @@ lazy_static! {
Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
}
#[derive(Clone, Debug, Serialize)]
pub struct ActixAdminError {
field_name: Option<String>,
error: String
}
// AppDataTrait
pub trait ActixAdminAppDataTrait {
fn get_db(&self) -> &DatabaseConnection;

View File

@ -3,6 +3,7 @@ use sea_orm::DatabaseConnection;
use serde::{Serialize};
use std::collections::HashMap;
use crate::ActixAdminViewModelField;
use crate::ActixAdminError;
#[async_trait]
pub trait ActixAdminModelTrait {
@ -12,12 +13,19 @@ pub trait ActixAdminModelTrait {
posts_per_page: usize,
) -> (usize, Vec<ActixAdminModel>);
fn get_fields() -> Vec<ActixAdminViewModelField>;
fn validate_model(model: &ActixAdminModel) -> Vec<ActixAdminError>;
// function to be overridable for custom error handling
fn validate(&self) -> Vec<ActixAdminError> {
return Vec::new()
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ActixAdminModel {
pub primary_key: Option<String>,
pub values: HashMap<String, String>,
pub errors: Vec<ActixAdminError>,
}
impl From<String> for ActixAdminModel {
@ -32,17 +40,31 @@ impl From<String> for ActixAdminModel {
);
}
ActixAdminModel { primary_key: None, values: hashmap }
ActixAdminModel { primary_key: None, values: hashmap, errors: Vec::new() }
}
}
impl ActixAdminModel {
pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Option<T> {
println!("{:?}", self.values);
let value = self.values.get(key).unwrap().to_string().parse::<T>();
match value {
Ok(val) => Some(val),
Err(_) => None, //panic!("key {} could not be parsed", key)
}
pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Result<Option<T>, ActixAdminError> {
let value = self.values.get(key);
let res: Result<Option<T>, ActixAdminError> = match value {
Some(val) => {
let parsed_val = val.parse::<T>();
match parsed_val {
Ok(val) => Ok(Some(val)),
Err(_) => Err(ActixAdminError {
field_name: Some(key.to_string()),
error: "Invalid Value".to_string()
})
}
},
_ => Ok(None)
};
res
}
pub fn has_errors(&self) -> bool {
return &self.errors.len() != &0
}
}

View File

@ -24,7 +24,7 @@
{%- endfor %}
<td>
<a href="edit/{{ entity.primary_key }}">&#9998;</a>
<a hx-post="delete/{{ entity.primary_key }}">&#128465;</a>
<a href="#" hx-post="delete/{{ entity.primary_key }}">&#128465;</a>
</td>
</tr>
{%- endfor %}

Binary file not shown.