implement field validation

This commit is contained in:
Manuel Gugger 2022-07-27 17:35:45 +02:00
parent 4d2d625d25
commit 1f88d67b76
11 changed files with 135 additions and 138 deletions

View File

@ -1,6 +1,7 @@
use syn::{ use syn::{
Visibility, Type Visibility, Type
}; };
use quote::ToTokens;
pub struct ModelField { pub struct ModelField {
pub visibility: Visibility, pub visibility: Visibility,
@ -17,4 +18,13 @@ impl ModelField {
pub fn is_option(&self) -> bool { pub fn is_option(&self) -> bool {
self.inner_type.is_some() self.inner_type.is_some()
} }
pub fn is_string(&self) -> bool {
match &self.ty {
Type::Path(type_path) if type_path.clone().into_token_stream().to_string() == "String" => {
true
}
_ => false
}
}
} }

View File

@ -1,10 +1,8 @@
use proc_macro2::{TokenStream};
use syn::{
Fields, DeriveInput, LitStr
};
use quote::quote;
use crate::attributes::derive_attr; use crate::attributes::derive_attr;
use crate::model_fields::{ ModelField }; use crate::model_fields::ModelField;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Fields, LitStr};
pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<ModelField> { pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<ModelField> {
let ast: DeriveInput = syn::parse(input).unwrap(); let ast: DeriveInput = syn::parse(input).unwrap();
@ -22,21 +20,28 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
fields fields
.iter() .iter()
.filter_map(|field| { .filter_map(|field| {
let actix_admin_attr = derive_attr::ActixAdmin::try_from_attributes(&field.attrs).unwrap_or_default(); let actix_admin_attr =
derive_attr::ActixAdmin::try_from_attributes(&field.attrs).unwrap_or_default();
if field.ident.is_some()
{ if field.ident.is_some() {
let field_vis = field.vis.clone(); let field_vis = field.vis.clone();
let field_ident = field.ident.as_ref().unwrap().clone(); let field_ident = field.ident.as_ref().unwrap().clone();
let inner_type = extract_type_from_option(&field.ty); let inner_type = extract_type_from_option(&field.ty);
let field_ty = field.ty.to_owned(); let field_ty = field.ty.to_owned();
let is_primary_key = actix_admin_attr.clone().map_or(false, |attr| attr.primary_key.is_some()); let is_primary_key = actix_admin_attr
let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| attr.select_list.map_or("".to_string(), .clone()
|attr_field| (LitStr::from(attr_field)).value() .map_or(false, |attr| attr.primary_key.is_some());
)); let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| {
let html_input_type = actix_admin_attr.map_or("text".to_string(), |attr| attr.html_input_type.map_or("text".to_string(), attr.select_list.map_or("".to_string(), |attr_field| {
|attr_field| (LitStr::from(attr_field)).value() (LitStr::from(attr_field)).value()
)); })
});
let html_input_type = actix_admin_attr.map_or("text".to_string(), |attr| {
attr.html_input_type
.map_or("text".to_string(), |attr_field| {
(LitStr::from(attr_field)).value()
})
});
let model_field = ModelField { let model_field = ModelField {
visibility: field_vis, visibility: field_vis,
@ -45,9 +50,8 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
inner_type: inner_type, inner_type: inner_type,
primary_key: is_primary_key, primary_key: is_primary_key,
html_input_type: html_input_type, html_input_type: html_input_type,
select_list: select_list select_list: select_list,
}; };
Some(model_field) Some(model_field)
} else { } else {
None None
@ -156,13 +160,12 @@ pub fn get_actix_admin_fields_select_list(fields: &Vec<ModelField>) -> Vec<Token
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn get_field_for_primary_key(fields: &Vec<ModelField>) -> TokenStream { pub fn get_field_for_primary_key(fields: &Vec<ModelField>) -> TokenStream {
let primary_key_model_field = fields let primary_key_model_field = fields
.iter() .iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr // TODO: filter id attr based on struct attr or sea_orm primary_key attr
.find(|model_field| model_field.primary_key) .find(|model_field| model_field.primary_key)
.expect("model must have a single primary key"); .expect("model must have a single primary key");
let ident = primary_key_model_field.ident.to_owned(); let ident = primary_key_model_field.ident.to_owned();
@ -173,60 +176,61 @@ pub fn get_field_for_primary_key(fields: &Vec<ModelField>) -> TokenStream {
pub fn get_primary_key_field_name(fields: &Vec<ModelField>) -> String { pub fn get_primary_key_field_name(fields: &Vec<ModelField>) -> String {
let primary_key_model_field = fields let primary_key_model_field = fields
.iter() .iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr // TODO: filter id attr based on struct attr or sea_orm primary_key attr
.find(|model_field| model_field.primary_key) .find(|model_field| model_field.primary_key)
.expect("model must have a single primary key"); .expect("model must have a single primary key");
primary_key_model_field.ident.to_string() primary_key_model_field.ident.to_string()
} }
pub fn get_fields_for_from_model(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_fields_for_from_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()
.filter(|model_field| !model_field.primary_key) .filter(|model_field| !model_field.primary_key)
.map(|model_field| { .map(|model_field| {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ident = model_field.ident.to_owned(); let ident = model_field.ident.to_owned();
match model_field.is_option() { match model_field.is_option() {
true => { true => {
quote! { quote! {
#ident_name => match model.#ident { #ident_name => match model.#ident {
Some(val) => val.to_string(), Some(val) => val.to_string(),
None => "".to_owned() None => "".to_owned()
}
}
}
false => {
quote! {
#ident_name => model.#ident.to_string()
}
} }
} }
}, })
false => { .collect::<Vec<_>>()
quote! {
#ident_name => model.#ident.to_string()
}
}
}
})
.collect::<Vec<_>>()
} }
pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr
.filter(|model_field| !model_field.primary_key) .filter(|model_field| !model_field.primary_key)
.map(|model_field| { .map(|model_field| {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
let is_option_or_string = model_field.is_option() || model_field.is_string();
match model_field.is_option() { match model_field.is_option() {
true => { true => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { quote! {
model.get_value::<#inner_ty>(#ident_name).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok(); model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
} }
}, },
false => { false => {
quote! { quote! {
model.get_value::<#ty>(#ident_name).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok(); model.get_value::<#ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
} }
} }
} }
@ -236,54 +240,72 @@ pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStrea
pub fn get_fields_for_create_model(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_fields_for_create_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr // TODO: filter id attr based on struct attr or sea_orm primary_key attr
.filter(|model_field| !model_field.primary_key) .filter(|model_field| !model_field.primary_key)
.map(|model_field| { .map(|model_field| {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ident = model_field.ident.to_owned(); let ident = model_field.ident.to_owned();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
match model_field.is_option() { let is_option_or_string = model_field.is_option() || model_field.is_string();
true => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); match model_field.is_option() {
quote! { true => {
#ident: Set(model.get_value::<#inner_ty>(#ident_name).unwrap()) let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! {
#ident: Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap())
}
} }
}, // TODO: this fails for empty string as it returns None which then cannot be unwrapped
false => { // Should do maybe typecheck and if it is string return "" instead of None
quote! { false => {
#ident: Set(model.get_value::<#ty>(#ident_name).unwrap().unwrap()) if model_field.is_string() {
quote! {
#ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new()))
}
} else {
quote! {
#ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap())
}
}
} }
} }
} })
}) .collect::<Vec<_>>()
.collect::<Vec<_>>()
} }
pub fn get_fields_for_edit_model(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_fields_for_edit_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()
// TODO: filter id attr based on struct attr or sea_orm primary_key attr // TODO: filter id attr based on struct attr or sea_orm primary_key attr
.filter(|model_field| !model_field.primary_key) .filter(|model_field| !model_field.primary_key)
.map(|model_field| { .map(|model_field| {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ident = model_field.ident.to_owned(); let ident = model_field.ident.to_owned();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
let is_option_or_string = model_field.is_option() || model_field.is_string();
match model_field.is_option() {
true => { match model_field.is_option() {
let inner_ty = model_field.inner_type.to_owned().unwrap(); true => {
quote! { let inner_ty = model_field.inner_type.to_owned().unwrap();
entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name).unwrap()) quote! {
entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap())
}
} }
}, false => {
false => { if model_field.is_string() {
quote! { quote! {
entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap().unwrap()) entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new()))
}
}
else {
quote! {
entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap())
}
}
} }
} }
} })
}) .collect::<Vec<_>>()
.collect::<Vec<_>>() }
}

View File

@ -2,7 +2,6 @@ use lazy_static::lazy_static;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera}; use tera::{Tera};
use serde::{Serialize};
pub mod view_model; pub mod view_model;
pub mod model; pub mod model;

View File

@ -49,14 +49,12 @@ impl From<String> for ActixAdminModel {
} }
impl ActixAdminModel { impl ActixAdminModel {
pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Result<Option<T>, String> { pub fn get_value<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool) -> Result<Option<T>, String> {
let value = self.values.get(key); let value = self.values.get(key);
println!("{:?}", key);
println!("{:?}", value);
let res: Result<Option<T>, String> = match value { let res: Result<Option<T>, String> = match value {
Some(val) => { Some(val) => {
if val.is_empty() { if val.is_empty() && is_option_or_string {
return Ok(None); return Ok(None);
} }
@ -67,7 +65,12 @@ impl ActixAdminModel {
Err(_) => Err("Invalid Value".to_string()), Err(_) => Err("Invalid Value".to_string()),
} }
} }
_ => Ok(None), _ => {
match is_option_or_string {
true => Ok(None),
false => Err("Invalid Value".to_string()) // a missing value in the form for a non-optional value
}
}
}; };
res res

View File

@ -28,7 +28,7 @@ pub async fn create_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
ctx.insert("model", &model); ctx.insert("model", &model);
let body = TERA let body = TERA
.render("edit.html", &ctx) .render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }

View File

@ -25,10 +25,9 @@ pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
ctx.insert("select_lists", &E::get_select_lists(db).await); ctx.insert("select_lists", &E::get_select_lists(db).await);
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", &model); ctx.insert("model", &model);
println!("{:?}", model.errors);
let body = TERA let body = TERA
.render("edit.html", &ctx) .render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }

View File

@ -29,7 +29,7 @@ pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
let body = TERA let body = TERA
.render("edit.html", &ctx) .render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }

View File

@ -29,7 +29,7 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
let body = TERA let body = TERA
.render("edit.html", &ctx) .render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }

View File

@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block content %}
<article>
<form method="post">
{% for model_field in view_model.fields -%}
<label for="{{ model_field.field_name }}">
{{ model_field.field_name }}
{% if model_field.select_list != "" %}
<select name="{{ model_field.field_name }}">
{% if model_field.is_option %}
<option value="" selected></option>
{% else %}
<option value="" selected disabled>Select</option>
{% endif %}
{% for select_list_item in select_lists[model_field.field_name] -%}
<option value="{{ select_list_item[0] }}">{{ select_list_item[1] }}</option>
{%- endfor %}
</select>
{% else %}
<input type="{{ model_field.html_input_type }}" name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}">
<!-- required="" -->
{% endif %}
</label>
{%- endfor %}
<button type="submit">Save</button>
<a href="{{ list_link }}" role="button" class="secondary">Cancel</a>
</form>
</article>
{% endblock content %}

View File

@ -2,11 +2,6 @@
{% block content %} {% block content %}
<article> <article>
<div>
<ul>
{{ model.errors }}
</ul>
</div>
<form method="post"> <form method="post">
<div> <div>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}

Binary file not shown.