derive fields for table columns
This commit is contained in:
parent
b5f55487fa
commit
790cf9ff59
@ -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" }
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
54
actix_admin/actix_admin_macros/src/struct_fields.rs
Normal file
54
actix_admin/actix_admin_macros/src/struct_fields.rs
Normal 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<_>>()
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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 %}
|
@ -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)]
|
||||||
|
@ -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")]
|
||||||
|
84
src/main.rs
84
src/main.rs
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user