diff --git a/actix_admin/Cargo.toml b/actix_admin/Cargo.toml index ac31fe6..871069e 100644 --- a/actix_admin/Cargo.toml +++ b/actix_admin/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] actix-web = "4.0.1" actix-rt = "2.7.0" -actix-session = "0.5.0" +actix-session = "0.6.2" tera = "1.15.0" actix_admin_macros = { path = "actix_admin_macros" } oauth2 = "4.1" diff --git a/actix_admin/actix_admin_macros/Cargo.toml b/actix_admin/actix_admin_macros/Cargo.toml index 8ef552e..e37c597 100644 --- a/actix_admin/actix_admin_macros/Cargo.toml +++ b/actix_admin/actix_admin_macros/Cargo.toml @@ -9,12 +9,13 @@ proc-macro = true [dependencies] actix-web = "4.0.1" actix-rt = "2.7.0" -actix-session = "0.5.0" +actix-session = "0.6.2" tera = "1.15.0" oauth2 = "4.1" base64 = "0.13.0" +bae = "0.1.7" quote = "1.0" syn = { version = "1.0", features = ["full", "extra-traits"] } proc-macro2 = { version = "1.0.36", default-features = false } diff --git a/actix_admin/actix_admin_macros/src/attributes.rs b/actix_admin/actix_admin_macros/src/attributes.rs new file mode 100644 index 0000000..554ef53 --- /dev/null +++ b/actix_admin/actix_admin_macros/src/attributes.rs @@ -0,0 +1,28 @@ +pub mod derive_attr { + use bae::FromAttributes; + + #[derive( + Debug, + Eq, + PartialEq, + FromAttributes, + Default + )] + pub struct ActixAdmin { + pub inner_type: Option, + + // 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, + //optional_given: Option, + + // A "switch" is something that doesn't take arguments. + // All fields with type `Option<()>` are considered swiches. + // They default to `None`. + //switch: Option<()>, + } +} \ No newline at end of file diff --git a/actix_admin/actix_admin_macros/src/lib.rs b/actix_admin/actix_admin_macros/src/lib.rs index c795c48..b196122 100644 --- a/actix_admin/actix_admin_macros/src/lib.rs +++ b/actix_admin/actix_admin_macros/src/lib.rs @@ -4,13 +4,15 @@ use quote::quote; mod struct_fields; 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 { let fields = get_fields_for_tokenstream(input); let names_const_fields_str = fields .iter() - .map(|(_vis, ident, _ty)| { + .map(|(_vis, ident, _ty, _is_option)| { let ident_name = ident.to_string(); quote! { #ident_name @@ -21,11 +23,20 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea let fields_for_create_model = fields .iter() // TODO: filter id attr based on struct attr or sea_orm primary_key attr - .filter(|(_vis, ident, _ty)| !ident.to_string().eq("id")) - .map(|(_vis, ident, ty)| { + .filter(|(_vis, ident, _ty, _is_option)| !ident.to_string().eq("id")) + .map(|(_vis, ident, ty, is_option)| { let ident_name = ident.to_string(); - quote! { - #ident: Set(model.get_value::<#ty>(#ident_name).unwrap()) + match is_option { + true => { + quote! { + #ident: Set(model.get_value::<#ty>(#ident_name)) + } + }, + false => { + quote! { + #ident: Set(model.get_value::<#ty>(#ident_name).unwrap()) + } + } } }) .collect::>(); @@ -33,22 +44,46 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea let fields_for_edit_model = fields .iter() // TODO: filter id attr based on struct attr or sea_orm primary_key attr - .filter(|(_vis, ident, _ty)| !ident.to_string().eq("id")) - .map(|(_vis, ident, ty)| { + .filter(|(_vis, ident, _ty, _is_option)| !ident.to_string().eq("id")) + .map(|(_vis, ident, ty, is_option)| { let ident_name = ident.to_string(); - quote! { - entity.#ident = Set(model.get_value::<#ty>(#ident_name).unwrap()) + println!("edit {} {:?}", &ident_name, ty); + 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::>(); let fields_for_from_model = fields .iter() - .map(|(_vis, ident, _ty)| { + .map(|(_vis, ident, _ty, is_option)| { let ident_name = ident.to_string(); - quote! { - #ident_name => model.#ident.to_string() + println!("from {} {:?}", &ident_name, _ty); + + 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::>(); @@ -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 { let new_model = ActiveModel::from(model.clone()); let insert_operation = Entity::insert(new_model).exec(db).await; + println!("creating {:?}", model); + println!("operation {:?}", insert_operation); model } diff --git a/actix_admin/actix_admin_macros/src/struct_fields.rs b/actix_admin/actix_admin_macros/src/struct_fields.rs index 83030f2..6ebe145 100644 --- a/actix_admin/actix_admin_macros/src/struct_fields.rs +++ b/actix_admin/actix_admin_macros/src/struct_fields.rs @@ -3,9 +3,11 @@ use syn::{ 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 (_vis, ty, _generics) = (&ast.vis, &ast.ident, &ast.generics); 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 { 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) { + //println!("2"); for nested_item in meta_list.nested.iter() { 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; } } @@ -32,24 +40,84 @@ pub fn has_skip_attr(attr: &Attribute, path: &'static str) -> bool { false } -pub fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident, Type)> { +pub fn get_field_type<'a>(actix_admin_attr: &'a Option, 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 .iter() .filter_map(|field| { + let actix_admin_attr = derive_attr::ActixAdmin::try_from_attributes(&field.attrs).unwrap_or_default(); + if field .attrs .iter() - .find(|attr| has_skip_attr(attr, "struct_field_names")) + .find(|attr| has_skip_attr(attr, ACTIX_ADMIN)) .is_none() && field.ident.is_some() { let field_vis = field.vis.clone(); let field_ident = field.ident.as_ref().unwrap().clone(); - let field_ty = field.ty.to_owned(); - Some((field_vis, field_ident, field_ty)) + println!("{}", field_ident.to_string()); + 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 { None } }) .collect::>() +} + +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 (""): + 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, + }) } \ No newline at end of file diff --git a/actix_admin/src/routes/delete_post.rs b/actix_admin/src/routes/delete_post.rs index d61b246..176edd7 100644 --- a/actix_admin/src/routes/delete_post.rs +++ b/actix_admin/src/routes/delete_post.rs @@ -1,4 +1,3 @@ -use actix_web::http::header; use actix_web::{web, Error, HttpRequest, HttpResponse}; use crate::prelude::*; @@ -6,12 +5,12 @@ use crate::prelude::*; pub async fn delete_post( _req: HttpRequest, data: web::Data, - text: String, + _text: String, id: web::Path ) -> Result { let db = &data.get_db(); - let entity_name = E::get_entity_name(); - let actix_admin = data.get_actix_admin(); + //let entity_name = E::get_entity_name(); + //let actix_admin = data.get_actix_admin(); //let view_model = actix_admin.view_models.get(&entity_name).unwrap(); // TODO:handle any errors diff --git a/actix_admin/src/routes/edit_get.rs b/actix_admin/src/routes/edit_get.rs index eae193e..e69a6b5 100644 --- a/actix_admin/src/routes/edit_get.rs +++ b/actix_admin/src/routes/edit_get.rs @@ -9,7 +9,7 @@ pub async fn edit_get( _req: HttpRequest, data: web::Data, _body: web::Payload, - text: String, + _text: String, id: web::Path ) -> Result { let db = &data.get_db(); diff --git a/actix_admin/src/routes/list.rs b/actix_admin/src/routes/list.rs index 2143dd9..a72c255 100644 --- a/actix_admin/src/routes/list.rs +++ b/actix_admin/src/routes/list.rs @@ -37,6 +37,7 @@ pub async fn list( let result: (usize, Vec) = E::list(db, page, entities_per_page).await; let entities = result.1; let num_pages = result.0; + println!("{:?}", entities); let mut ctx = Context::new(); ctx.insert("entity_names", &entity_names); diff --git a/database.db b/database.db index 1060506..3d1e885 100644 Binary files a/database.db and b/database.db differ diff --git a/database.db-wal b/database.db-wal index 6cdae97..6c52d74 100644 Binary files a/database.db-wal and b/database.db-wal differ diff --git a/src/entity/mod.rs b/src/entity/mod.rs index d999ecb..5c8e320 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -25,6 +25,8 @@ pub async fn create_post_table(db: &DbConn) -> Result { ) .col(ColumnDef::new(post::Column::Title).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(); create_table(db, &stmt).await; diff --git a/src/entity/post.rs b/src/entity/post.rs index f1b32f9..e69c7ec 100644 --- a/src/entity/post.rs +++ b/src/entity/post.rs @@ -1,6 +1,9 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; use actix_admin::prelude::*; +use std::str::FromStr; +use std::fmt; +use std::fmt::Display; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdminModel)] #[sea_orm(table_name = "post")] @@ -11,9 +14,42 @@ pub struct Model { pub title: String, #[sea_orm(column_type = "Text")] pub text: String, + pub tea_mandatory: Tea, + #[actix_admin(inner_type=Tea)] + pub tea_optional: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} -impl ActiveModelBehavior for ActiveModel {} \ No newline at end of file +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 { + 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")), + } + } +} \ No newline at end of file