derive fields for table columns

This commit is contained in:
Manuel Gugger 2022-04-27 18:22:47 +02:00
parent b5f55487fa
commit 790cf9ff59
8 changed files with 178 additions and 58 deletions

View File

@ -24,6 +24,6 @@ serde_json = "1.0.79"
serde_derive = "1.0.136" serde_derive = "1.0.136"
quote = "1.0" quote = "1.0"
sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false } sea-orm = { version = "0.6.0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = false }
syn = "1.0.91"
actix_admin = { path = "actix_admin" } actix_admin = { path = "actix_admin" }
azure_auth = { path = "azure_auth" } azure_auth = { path = "azure_auth" }

View File

@ -1,22 +1,48 @@
use proc_macro; use proc_macro;
use quote::quote; use quote::quote;
use proc_macro2::{Span, Ident};
use syn::{ DeriveInput };
mod struct_fields;
use struct_fields::get_field_for_tokenstream;
#[proc_macro_derive(DeriveActixAdminModel)] #[proc_macro_derive(DeriveActixAdminModel)]
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_field_for_tokenstream(input);
let names_const_fields_str = fields.iter().map(|(_vis, ident)| {
let ident_name = ident.to_string();
quote! {
#ident_name
}
});
let expanded = quote! { let expanded = quote! {
use std::convert::From; use std::convert::From;
use async_trait::async_trait; use async_trait::async_trait;
use actix_web::{web, HttpResponse, HttpRequest, Error}; use actix_web::{web, HttpResponse, HttpRequest, Error};
use actix_admin::{ ActixAdminModelTrait, ActixAdminViewModelTrait, ActixAdminViewModel, ActixAdminModel, AppDataTrait }; use actix_admin::{ ActixAdminField, ActixAdminModelTrait, ActixAdminViewModelTrait, ActixAdminViewModel, ActixAdminModel, AppDataTrait };
impl From<Entity> for ActixAdminViewModel { impl From<Entity> for ActixAdminViewModel {
fn from(entity: Entity) -> Self { fn from(entity: Entity) -> Self {
ActixAdminViewModel { ActixAdminViewModel {
entity_name: entity.table_name().to_string() entity_name: entity.table_name().to_string(),
fields: Entity::get_fields()
} }
} }
} }
#[async_trait(?Send)]
impl ActixAdminViewModelTrait for Entity {
async fn list<T: AppDataTrait>(req: HttpRequest, data: web::Data<T>) -> Result<HttpResponse, Error> {
let db = &data.get_db();
let entities = Entity::list_db(db, 1, 5);
let entity_names = &data.get_actix_admin().entity_names;
let model = ActixAdminViewModel::from(Entity);
actix_admin::list_model(req, &data, model, entity_names)
}
}
#[async_trait] #[async_trait]
impl ActixAdminModelTrait for Entity { impl ActixAdminModelTrait for Entity {
async fn list_db(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<&str> { async fn list_db(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<&str> {
@ -33,16 +59,23 @@ pub fn derive_crud_fns(_input: proc_macro::TokenStream) -> proc_macro::TokenStre
] ]
} }
}
#[async_trait(?Send)] fn get_fields() -> Vec<(&'static str, ActixAdminField)> {
impl ActixAdminViewModelTrait for Entity { let mut vec = Vec::new();
async fn list<T: AppDataTrait>(req: HttpRequest, data: web::Data<T>) -> Result<HttpResponse, Error> { let field_names = stringify!(
let db = &data.get_db(); #(#names_const_fields_str),*
let entities = Entity::list_db(db, 1, 5); ).split(",")
let entity_names = &data.get_actix_admin().entity_names; .collect::<Vec<_>>()
let model = ActixAdminViewModel::from(Entity); .into_iter()
actix_admin::list_model(req, &data, model, entity_names) .for_each( |field_name|
vec.push((
field_name,
// TODO: derive correct AxtixAdminField Value
ActixAdminField::Text
)
)
);
vec
} }
} }
}; };

View File

@ -0,0 +1,54 @@
use proc_macro2::{Span, Ident};
use syn::{
Attribute, Fields, Meta, NestedMeta, Visibility, DeriveInput
};
const ATTR_META_SKIP: &'static str = "skip";
pub fn get_field_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::Vec<(syn::Visibility, proc_macro2::Ident)> {
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());
let fields = filter_fields(match ast.data {
syn::Data::Struct(ref s) => &s.fields,
_ => panic!("FieldNames can only be derived for structs"),
});
fields
}
pub fn has_skip_attr(attr: &Attribute, path: &'static str) -> bool {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if meta_list.path.is_ident(path) {
for nested_item in meta_list.nested.iter() {
if let NestedMeta::Meta(Meta::Path(path)) = nested_item {
if path.is_ident(ATTR_META_SKIP) {
return true;
}
}
}
}
}
false
}
pub fn filter_fields(fields: &Fields) -> Vec<(Visibility, Ident)> {
fields
.iter()
.filter_map(|field| {
if field
.attrs
.iter()
.find(|attr| has_skip_attr(attr, "struct_field_names"))
.is_none()
&& field.ident.is_some()
{
let field_vis = field.vis.clone();
let field_ident = field.ident.as_ref().unwrap().clone();
Some((field_vis, field_ident))
} else {
None
}
})
.collect::<Vec<_>>()
}

View File

@ -33,7 +33,7 @@ pub struct Params {
// Fields // Fields
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub enum Field { pub enum ActixAdminField {
Text, Text,
} }
@ -47,22 +47,25 @@ pub trait AppDataTrait {
#[async_trait] #[async_trait]
pub trait ActixAdminModelTrait: Clone { pub trait ActixAdminModelTrait: Clone {
async fn list_db(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<&str>; async fn list_db(db: &DatabaseConnection, page: usize, posts_per_page: usize) -> Vec<&str>;
fn get_fields() -> Vec<(&'static str, ActixAdminField)>;
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct ActixAdminModel { pub struct ActixAdminModel {
pub fields: Vec<(&'static str, Field)>,
} }
// ActixAdminViewModel // ActixAdminViewModel
#[async_trait(?Send)] #[async_trait(?Send)]
pub trait ActixAdminViewModelTrait { pub trait ActixAdminViewModelTrait {
async fn list<T: AppDataTrait + Sync + Send>(req: HttpRequest, data: web::Data<T>) -> Result<HttpResponse, Error>; async fn list<T: AppDataTrait + Sync + Send>(req: HttpRequest, data: web::Data<T>) -> Result<HttpResponse, Error>;
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct ActixAdminViewModel { pub struct ActixAdminViewModel {
pub entity_name: String, pub entity_name: String,
pub fields: Vec<(&'static str, ActixAdminField)>,
} }
// ActixAdminController // ActixAdminController
@ -120,6 +123,7 @@ pub fn list_model<T: AppDataTrait>(req: HttpRequest, data: &web::Data<T>, view_m
ctx.insert("posts_per_page", &posts_per_page); ctx.insert("posts_per_page", &posts_per_page);
ctx.insert("num_pages", "5" /*&num_pages*/); ctx.insert("num_pages", "5" /*&num_pages*/);
ctx.insert("columns", &columns); ctx.insert("columns", &columns);
ctx.insert("model_fields", &view_model.fields);
let body = TERA let body = TERA
.render("list.html", &ctx) .render("list.html", &ctx)

View File

@ -1,5 +1,14 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
Hello <table>
<tr>
{% for model_field in model_fields -%}
<th>{{ model_field[0] }}</th>
{%- endfor %}
</tr>
<tr>
<td></td>
</tr>
</table>
{% endblock content %} {% endblock content %}

View File

@ -9,9 +9,9 @@ pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub id: i32, pub id: i32,
pub title: String, pub comment: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
pub text: String, pub user: String,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -1,6 +1,8 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use actix_admin::{ DeriveActixAdminModel }; use actix_admin::{ DeriveActixAdminModel };
use sea_orm::{entity::*, query::*, tests_cfg::cake};
#[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")]

View File

@ -1,25 +1,28 @@
extern crate serde_derive; extern crate serde_derive;
use actix_session::{Session, CookieSession}; use actix_admin::{
ActixAdmin, ActixAdminViewModel, ActixAdminViewModelTrait,
AppDataTrait as ActixAdminAppDataTrait,
};
use actix_session::{CookieSession, Session};
use actix_web::{web, App, HttpResponse, HttpServer}; use actix_web::{web, App, HttpResponse, HttpServer};
use tera::{ Tera, Context}; use azure_auth::{AppDataTrait as AzureAuthAppDataTrait, AzureAuth, UserInfo};
use oauth2::basic::BasicClient; use oauth2::basic::BasicClient;
use oauth2::{ RedirectUrl }; use oauth2::RedirectUrl;
use std::time::{Duration}; use sea_orm::{ConnectOptions, DatabaseConnection};
use std::env; use std::env;
use sea_orm::{{ DatabaseConnection, ConnectOptions }}; use std::time::Duration;
use actix_admin::{ AppDataTrait as ActixAdminAppDataTrait, ActixAdminViewModel, ActixAdmin, ActixAdminViewModelTrait }; use tera::{Context, Tera};
use azure_auth::{ AzureAuth, UserInfo, AppDataTrait as AzureAuthAppDataTrait };
mod entity; mod entity;
use entity::{ Post, Comment }; use entity::{Comment, Post};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AppState { pub struct AppState {
pub oauth: BasicClient, pub oauth: BasicClient,
pub tmpl: Tera, pub tmpl: Tera,
pub db: DatabaseConnection, pub db: DatabaseConnection,
pub actix_admin: ActixAdmin pub actix_admin: ActixAdmin,
} }
impl ActixAdminAppDataTrait for AppState { impl ActixAdminAppDataTrait for AppState {
@ -39,7 +42,11 @@ impl AzureAuthAppDataTrait for AppState {
async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse { async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse {
let login = session.get::<UserInfo>("user_info").unwrap(); let login = session.get::<UserInfo>("user_info").unwrap();
let web_auth_link = if login.is_some() { "/auth/logout" } else { "/auth/login" }; let web_auth_link = if login.is_some() {
"/auth/logout"
} else {
"/auth/login"
};
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("web_auth_link", web_auth_link); ctx.insert("web_auth_link", web_auth_link);
@ -47,26 +54,46 @@ async fn index(session: Session, data: web::Data<AppState>) -> HttpResponse {
HttpResponse::Ok().body(rendered) HttpResponse::Ok().body(rendered)
} }
// TODO: Generate this with a Macro accepting Tuples of (Entity, viewmodel)
fn setup_actix_admin(
actix_admin: &ActixAdmin,
post_view_model: &ActixAdminViewModel,
comment_view_model: &ActixAdminViewModel,
) -> actix_web::Scope {
actix_admin
.create_scope::<AppState>()
.service(
web::scope(&format!("/{}", post_view_model.entity_name))
.route("/list", web::get().to(Post::list::<AppState>)),
)
.service(
web::scope(&format!("/{}", comment_view_model.entity_name))
.route("/list", web::get().to(Comment::list::<AppState>)),
)
}
#[actix_rt::main] #[actix_rt::main]
async fn main() { async fn main() {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let oauth2_client_id = env::var("OAUTH2_CLIENT_ID").expect("Missing the OAUTH2_CLIENT_ID environment variable."); let oauth2_client_id =
let oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET").expect("Missing the OAUTH2_CLIENT_SECRET environment variable."); env::var("OAUTH2_CLIENT_ID").expect("Missing the OAUTH2_CLIENT_ID environment variable.");
let oauth2_server = env::var("OAUTH2_SERVER").expect("Missing the OAUTH2_SERVER environment variable."); let oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET")
.expect("Missing the OAUTH2_CLIENT_SECRET environment variable.");
let oauth2_server =
env::var("OAUTH2_SERVER").expect("Missing the OAUTH2_SERVER environment variable.");
let azure_auth = AzureAuth::new(&oauth2_server, &oauth2_client_id, &oauth2_client_secret); let azure_auth = AzureAuth::new(&oauth2_server, &oauth2_client_id, &oauth2_client_secret);
// Set up the config for the OAuth2 process. // Set up the config for the OAuth2 process.
let client = azure_auth.clone().get_oauth_client() let client = azure_auth
.clone()
.get_oauth_client()
// This example will be running its own server at 127.0.0.1:5000. // This example will be running its own server at 127.0.0.1:5000.
.set_redirect_uri( .set_redirect_uri(
RedirectUrl::new("http://localhost:5000/auth".to_string()) RedirectUrl::new("http://localhost:5000/auth".to_string())
.expect("Invalid redirect URL"), .expect("Invalid redirect URL"),
); );
let tera = let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
Tera::new(
concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")
).unwrap();
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file");
let mut opt = ConnectOptions::new(db_url); let mut opt = ConnectOptions::new(db_url);
@ -84,12 +111,11 @@ async fn main() {
let actix_admin = ActixAdmin::new() let actix_admin = ActixAdmin::new()
.add_entity::<AppState>(&post_view_model) .add_entity::<AppState>(&post_view_model)
.add_entity::<AppState>(&comment_view_model); .add_entity::<AppState>(&comment_view_model);
let app_state = AppState { let app_state = AppState {
oauth: client, oauth: client,
tmpl: tera, tmpl: tera,
db: conn, db: conn,
actix_admin: actix_admin.clone() actix_admin: actix_admin.clone(),
}; };
HttpServer::new(move || { HttpServer::new(move || {
@ -98,19 +124,11 @@ async fn main() {
.wrap(CookieSession::signed(&[0; 32]).secure(false)) .wrap(CookieSession::signed(&[0; 32]).secure(false))
.route("/", web::get().to(index)) .route("/", web::get().to(index))
.service(azure_auth.clone().create_scope::<AppState>()) .service(azure_auth.clone().create_scope::<AppState>())
.service( .service(setup_actix_admin(
// TODO: Generate this with a Macro accepting Tuples of (Entity, viewmodel) &actix_admin,
actix_admin &post_view_model,
.create_scope::<AppState>() &comment_view_model,
.service( ))
web::scope(&format!("/{}", post_view_model.entity_name))
.route("/list", web::get().to(Post::list::<AppState>)),
)
.service(
web::scope(&format!("/{}", comment_view_model.entity_name))
.route("/list", web::get().to(Comment::list::<AppState>)),
)
)
}) })
.bind("127.0.0.1:5000") .bind("127.0.0.1:5000")
.expect("Can not bind to port 5000") .expect("Can not bind to port 5000")