add struct attr and parsing for option
This commit is contained in:
parent
9b5bc5c4da
commit
244d1f9e01
@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.0.1"
|
actix-web = "4.0.1"
|
||||||
actix-rt = "2.7.0"
|
actix-rt = "2.7.0"
|
||||||
actix-session = "0.5.0"
|
actix-session = "0.6.2"
|
||||||
tera = "1.15.0"
|
tera = "1.15.0"
|
||||||
actix_admin_macros = { path = "actix_admin_macros" }
|
actix_admin_macros = { path = "actix_admin_macros" }
|
||||||
oauth2 = "4.1"
|
oauth2 = "4.1"
|
||||||
|
@ -9,12 +9,13 @@ proc-macro = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.0.1"
|
actix-web = "4.0.1"
|
||||||
actix-rt = "2.7.0"
|
actix-rt = "2.7.0"
|
||||||
actix-session = "0.5.0"
|
actix-session = "0.6.2"
|
||||||
tera = "1.15.0"
|
tera = "1.15.0"
|
||||||
|
|
||||||
oauth2 = "4.1"
|
oauth2 = "4.1"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
|
|
||||||
|
bae = "0.1.7"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
proc-macro2 = { version = "1.0.36", default-features = false }
|
proc-macro2 = { version = "1.0.36", default-features = false }
|
||||||
|
28
actix_admin/actix_admin_macros/src/attributes.rs
Normal file
28
actix_admin/actix_admin_macros/src/attributes.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
pub mod derive_attr {
|
||||||
|
use bae::FromAttributes;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Eq,
|
||||||
|
PartialEq,
|
||||||
|
FromAttributes,
|
||||||
|
Default
|
||||||
|
)]
|
||||||
|
pub struct ActixAdmin {
|
||||||
|
pub inner_type: Option<syn::Type>,
|
||||||
|
|
||||||
|
// Anything that implements `syn::parse::Parse` is supported.
|
||||||
|
//mandatory_type: syn::Type,
|
||||||
|
//mandatory_ident: syn::Ident,
|
||||||
|
|
||||||
|
// Fields wrapped in `Option` are optional and default to `None` if
|
||||||
|
// not specified in the attribute.
|
||||||
|
//optional_missing: Option<syn::Type>,
|
||||||
|
//optional_given: Option<syn::Type>,
|
||||||
|
|
||||||
|
// A "switch" is something that doesn't take arguments.
|
||||||
|
// All fields with type `Option<()>` are considered swiches.
|
||||||
|
// They default to `None`.
|
||||||
|
//switch: Option<()>,
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,15 @@ use quote::quote;
|
|||||||
mod struct_fields;
|
mod struct_fields;
|
||||||
use struct_fields::get_fields_for_tokenstream;
|
use struct_fields::get_fields_for_tokenstream;
|
||||||
|
|
||||||
#[proc_macro_derive(DeriveActixAdminModel)]
|
mod attributes;
|
||||||
|
|
||||||
|
#[proc_macro_derive(DeriveActixAdminModel, attributes(actix_admin))]
|
||||||
pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
let fields = get_fields_for_tokenstream(input);
|
let fields = get_fields_for_tokenstream(input);
|
||||||
|
|
||||||
let names_const_fields_str = fields
|
let names_const_fields_str = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_vis, ident, _ty)| {
|
.map(|(_vis, ident, _ty, _is_option)| {
|
||||||
let ident_name = ident.to_string();
|
let ident_name = ident.to_string();
|
||||||
quote! {
|
quote! {
|
||||||
#ident_name
|
#ident_name
|
||||||
@ -21,11 +23,20 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
|
|||||||
let fields_for_create_model = fields
|
let fields_for_create_model = 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(|(_vis, ident, _ty)| !ident.to_string().eq("id"))
|
.filter(|(_vis, ident, _ty, _is_option)| !ident.to_string().eq("id"))
|
||||||
.map(|(_vis, ident, ty)| {
|
.map(|(_vis, ident, ty, is_option)| {
|
||||||
let ident_name = ident.to_string();
|
let ident_name = ident.to_string();
|
||||||
quote! {
|
match is_option {
|
||||||
#ident: Set(model.get_value::<#ty>(#ident_name).unwrap())
|
true => {
|
||||||
|
quote! {
|
||||||
|
#ident: Set(model.get_value::<#ty>(#ident_name))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => {
|
||||||
|
quote! {
|
||||||
|
#ident: Set(model.get_value::<#ty>(#ident_name).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -33,22 +44,46 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
|
|||||||
let fields_for_edit_model = fields
|
let fields_for_edit_model = 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(|(_vis, ident, _ty)| !ident.to_string().eq("id"))
|
.filter(|(_vis, ident, _ty, _is_option)| !ident.to_string().eq("id"))
|
||||||
.map(|(_vis, ident, ty)| {
|
.map(|(_vis, ident, ty, is_option)| {
|
||||||
let ident_name = ident.to_string();
|
let ident_name = ident.to_string();
|
||||||
quote! {
|
println!("edit {} {:?}", &ident_name, ty);
|
||||||
entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap())
|
match is_option {
|
||||||
|
true => {
|
||||||
|
quote! {
|
||||||
|
entity.#ident = Set(model.get_value::<#ty>(#ident_name))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => {
|
||||||
|
quote! {
|
||||||
|
entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let fields_for_from_model = fields
|
let fields_for_from_model = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_vis, ident, _ty)| {
|
.map(|(_vis, ident, _ty, is_option)| {
|
||||||
let ident_name = ident.to_string();
|
let ident_name = ident.to_string();
|
||||||
quote! {
|
println!("from {} {:?}", &ident_name, _ty);
|
||||||
#ident_name => model.#ident.to_string()
|
|
||||||
|
match is_option {
|
||||||
|
true => {
|
||||||
|
quote! {
|
||||||
|
#ident_name => match model.#ident {
|
||||||
|
Some(val) => val.to_string(),
|
||||||
|
None => "".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false => {
|
||||||
|
quote! {
|
||||||
|
#ident_name => model.#ident.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@ -104,6 +139,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
|
|||||||
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel {
|
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel {
|
||||||
let new_model = ActiveModel::from(model.clone());
|
let new_model = ActiveModel::from(model.clone());
|
||||||
let insert_operation = Entity::insert(new_model).exec(db).await;
|
let insert_operation = Entity::insert(new_model).exec(db).await;
|
||||||
|
println!("creating {:?}", model);
|
||||||
|
println!("operation {:?}", insert_operation);
|
||||||
|
|
||||||
model
|
model
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@ use syn::{
|
|||||||
Attribute, Fields, Meta, NestedMeta, Visibility, DeriveInput, Type
|
Attribute, Fields, Meta, NestedMeta, Visibility, DeriveInput, Type
|
||||||
};
|
};
|
||||||
|
|
||||||
const ATTR_META_SKIP: &'static str = "skip";
|
use crate::attributes::derive_attr;
|
||||||
|
|
||||||
pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<(syn::Visibility, proc_macro2::Ident, Type)> {
|
const ACTIX_ADMIN: &'static str = "actix_admin";
|
||||||
|
|
||||||
|
pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<(syn::Visibility, proc_macro2::Ident, Type, bool)> {
|
||||||
let ast: DeriveInput = syn::parse(input).unwrap();
|
let ast: DeriveInput = syn::parse(input).unwrap();
|
||||||
let (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics);
|
let (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics);
|
||||||
let _names_struct_ident = Ident::new(&(ty.to_string() + "FieldStaticStr"), Span::call_site());
|
let _names_struct_ident = Ident::new(&(ty.to_string() + "FieldStaticStr"), Span::call_site());
|
||||||
@ -19,10 +21,16 @@ pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::V
|
|||||||
|
|
||||||
pub fn has_skip_attr(attr: &Attribute, path: &'static str) -> bool {
|
pub fn has_skip_attr(attr: &Attribute, path: &'static str) -> bool {
|
||||||
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
|
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
|
||||||
|
//println!("1");
|
||||||
|
//println!("{:?}", meta_list.path);
|
||||||
|
//println!("{}", path);
|
||||||
if meta_list.path.is_ident(path) {
|
if meta_list.path.is_ident(path) {
|
||||||
|
//println!("2");
|
||||||
for nested_item in meta_list.nested.iter() {
|
for nested_item in meta_list.nested.iter() {
|
||||||
if let NestedMeta::Meta(Meta::Path(path)) = nested_item {
|
if let NestedMeta::Meta(Meta::Path(path)) = nested_item {
|
||||||
if path.is_ident(ATTR_META_SKIP) {
|
//println!("3");
|
||||||
|
if path.is_ident(ACTIX_ADMIN) {
|
||||||
|
//println!("true");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,24 +40,84 @@ pub fn has_skip_attr(attr: &Attribute, path: &'static str) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident, Type)> {
|
pub fn get_field_type<'a>(actix_admin_attr: &'a Option<derive_attr::ActixAdmin>, field: &'a syn::Field) -> &'a syn::Type {
|
||||||
|
match actix_admin_attr {
|
||||||
|
Some(attr) => {
|
||||||
|
match &attr.inner_type {
|
||||||
|
Some(inner_type) => &inner_type,
|
||||||
|
None => &field.ty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => &field.ty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident, Type, bool)> {
|
||||||
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();
|
||||||
|
|
||||||
if field
|
if field
|
||||||
.attrs
|
.attrs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|attr| has_skip_attr(attr, "struct_field_names"))
|
.find(|attr| has_skip_attr(attr, ACTIX_ADMIN))
|
||||||
.is_none()
|
.is_none()
|
||||||
&& field.ident.is_some()
|
&& 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 field_ty = field.ty.to_owned();
|
println!("{}", field_ident.to_string());
|
||||||
Some((field_vis, field_ident, field_ty))
|
let is_option = extract_type_from_option(&field.ty).is_some();
|
||||||
|
let field_ty = get_field_type(&actix_admin_attr, &field).to_owned();
|
||||||
|
Some((field_vis, field_ident, field_ty, is_option))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_type_from_option(ty: &syn::Type) -> Option<&syn::Type> {
|
||||||
|
use syn::{GenericArgument, Path, PathArguments, PathSegment};
|
||||||
|
|
||||||
|
fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
|
||||||
|
match *ty {
|
||||||
|
syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO store (with lazy static) the vec of string
|
||||||
|
// TODO maybe optimization, reverse the order of segments
|
||||||
|
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
|
||||||
|
let idents_of_path = path
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.into_iter()
|
||||||
|
.fold(String::new(), |mut acc, v| {
|
||||||
|
acc.push_str(&v.ident.to_string());
|
||||||
|
acc.push('|');
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
vec!["Option|", "std|option|Option|", "core|option|Option|"]
|
||||||
|
.into_iter()
|
||||||
|
.find(|s| &idents_of_path == *s)
|
||||||
|
.and_then(|_| path.segments.last())
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_type_path(ty)
|
||||||
|
.and_then(|path| extract_option_segment(path))
|
||||||
|
.and_then(|path_seg| {
|
||||||
|
let type_params = &path_seg.arguments;
|
||||||
|
// It should have only on angle-bracketed param ("<String>"):
|
||||||
|
match *type_params {
|
||||||
|
PathArguments::AngleBracketed(ref params) => params.args.first(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.and_then(|generic_arg| match *generic_arg {
|
||||||
|
GenericArgument::Type(ref ty) => Some(ty),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
use actix_web::http::header;
|
|
||||||
use actix_web::{web, Error, HttpRequest, HttpResponse};
|
use actix_web::{web, Error, HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@ -6,12 +5,12 @@ use crate::prelude::*;
|
|||||||
pub async fn delete_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
pub async fn delete_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||||
_req: HttpRequest,
|
_req: HttpRequest,
|
||||||
data: web::Data<T>,
|
data: web::Data<T>,
|
||||||
text: String,
|
_text: String,
|
||||||
id: web::Path<i32>
|
id: web::Path<i32>
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let db = &data.get_db();
|
let db = &data.get_db();
|
||||||
let entity_name = E::get_entity_name();
|
//let entity_name = E::get_entity_name();
|
||||||
let actix_admin = data.get_actix_admin();
|
//let actix_admin = data.get_actix_admin();
|
||||||
//let view_model = actix_admin.view_models.get(&entity_name).unwrap();
|
//let view_model = actix_admin.view_models.get(&entity_name).unwrap();
|
||||||
|
|
||||||
// TODO:handle any errors
|
// TODO:handle any errors
|
||||||
|
@ -9,7 +9,7 @@ pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
|||||||
_req: HttpRequest,
|
_req: HttpRequest,
|
||||||
data: web::Data<T>,
|
data: web::Data<T>,
|
||||||
_body: web::Payload,
|
_body: web::Payload,
|
||||||
text: String,
|
_text: String,
|
||||||
id: web::Path<i32>
|
id: web::Path<i32>
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let db = &data.get_db();
|
let db = &data.get_db();
|
||||||
|
@ -37,6 +37,7 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
|||||||
let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page).await;
|
let result: (usize, Vec<ActixAdminModel>) = E::list(db, page, entities_per_page).await;
|
||||||
let entities = result.1;
|
let entities = result.1;
|
||||||
let num_pages = result.0;
|
let num_pages = result.0;
|
||||||
|
println!("{:?}", entities);
|
||||||
|
|
||||||
let mut ctx = Context::new();
|
let mut ctx = Context::new();
|
||||||
ctx.insert("entity_names", &entity_names);
|
ctx.insert("entity_names", &entity_names);
|
||||||
|
BIN
database.db
BIN
database.db
Binary file not shown.
BIN
database.db-wal
BIN
database.db-wal
Binary file not shown.
@ -25,6 +25,8 @@ pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
|||||||
)
|
)
|
||||||
.col(ColumnDef::new(post::Column::Title).string().not_null())
|
.col(ColumnDef::new(post::Column::Title).string().not_null())
|
||||||
.col(ColumnDef::new(post::Column::Text).string().not_null())
|
.col(ColumnDef::new(post::Column::Text).string().not_null())
|
||||||
|
.col(ColumnDef::new(post::Column::TeaMandatory).string().not_null())
|
||||||
|
.col(ColumnDef::new(post::Column::TeaOptional).string())
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
create_table(db, &stmt).await;
|
create_table(db, &stmt).await;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use actix_admin::prelude::*;
|
use actix_admin::prelude::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)]
|
||||||
#[sea_orm(table_name = "post")]
|
#[sea_orm(table_name = "post")]
|
||||||
@ -11,9 +14,42 @@ pub struct Model {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
#[sea_orm(column_type = "Text")]
|
#[sea_orm(column_type = "Text")]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
pub tea_mandatory: Tea,
|
||||||
|
#[actix_admin(inner_type=Tea)]
|
||||||
|
pub tea_optional: Option<Tea>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
|
||||||
|
pub enum Tea {
|
||||||
|
#[sea_orm(string_value = "EverydayTea")]
|
||||||
|
EverydayTea,
|
||||||
|
#[sea_orm(string_value = "BreakfastTea")]
|
||||||
|
BreakfastTea,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Tea {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Tea, Self::Err> {
|
||||||
|
match input {
|
||||||
|
"EverydayTea" => Ok(Tea::EverydayTea),
|
||||||
|
"BreakfastTea" => Ok(Tea::BreakfastTea),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Tea {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match &*self {
|
||||||
|
Tea::EverydayTea => write!(formatter, "{}", String::from("EverydayTea")),
|
||||||
|
Tea::BreakfastTea => write!(formatter, "{}", String::from("BreakfastTea")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user