1use proc_macro;
6use quote::quote;
7
8mod struct_fields;
9use struct_fields::*;
10
11mod selectlist_fields;
12use selectlist_fields::{get_select_list_from_enum, get_select_list_from_model, get_select_lists};
13
14mod attributes;
15mod model_fields;
16
17#[proc_macro_derive(DeriveActixAdminEnumSelectList, attributes(actix_admin))]
18pub fn derive_actix_admin_enum_select_list(
19 input: proc_macro::TokenStream,
20) -> proc_macro::TokenStream {
21 get_select_list_from_enum(input)
22}
23
24#[proc_macro_derive(DeriveActixAdminModelSelectList, attributes(actix_admin))]
25pub fn derive_actix_admin_model_select_list(
26 input: proc_macro::TokenStream,
27) -> proc_macro::TokenStream {
28 get_select_list_from_model(input)
29}
30
31#[proc_macro_derive(DeriveActixAdmin, attributes(actix_admin))]
32pub fn derive_actix_admin(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
33 let expanded = quote! {
34 use std::convert::From;
35 use actix_admin::prelude::*;
36 use sea_orm::{
37 ActiveValue::Set,
38 ConnectOptions,
39 DatabaseConnection,
40 entity::*,
41 query::*,
42 EntityTrait
43 };
44 use std::collections::HashMap;
45 use regex::Regex;
46 };
47 proc_macro::TokenStream::from(expanded)
48}
49
50#[proc_macro_derive(DeriveActixAdminViewModel, attributes(actix_admin))]
51pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
52 let fields = get_fields_for_tokenstream(input);
53
54 let name_primary_field_str = get_primary_key_field_name(&fields);
55 let primary_key_column = get_primary_key_column_ident(&fields);
56 let primary_key_type = get_primary_key_type(&fields);
57 let fields_for_edit_model = get_fields_for_edit_model(&fields);
58 let fields_searchable = get_actix_admin_fields_searchable(&fields);
59 let has_searchable_fields = fields_searchable.len() > 0;
60
61 let select_lists = get_select_lists(&fields);
62
63 let tenant_ref_field = get_tenant_ref_field(&fields, false);
64
65 let set_tenant_ref_field = get_set_tenant_ref_field(&fields);
66
67 let expanded = quote! {
68 impl From<Entity> for ActixAdminViewModel {
69 fn from(entity: Entity) -> Self {
70 ActixAdminViewModel {
71 primary_key: #name_primary_field_str.to_string(),
72 entity_name: entity.table_name().to_string(),
73 fields: Entity::get_fields(),
74 show_search: #has_searchable_fields,
75 user_can_access: None,
76 user_can_create: None,
77 user_can_edit: None,
78 user_can_delete: None,
79 user_can_view_details: None,
80 user_can_export: None,
81 default_show_aside: Entity::get_filter().len() > 0,
82 inline_edit: false,
83 bulk_actions: Vec::new(),
84 }
85 }
86 }
87
88 #[actix_admin::prelude::async_trait(?Send)]
89 impl ActixAdminViewModelTrait for Entity {
90 type Id = #primary_key_type;
91
92 async fn list(db: &DatabaseConnection, params: &ActixAdminViewModelParams) -> Result<(Option<u64>, Vec<ActixAdminModel>), ActixAdminError> {
93 let filter_values: HashMap<String, Option<String>> = params.viewmodel_filter.iter().map(|f| (f.name.to_string(), f.value.clone())).collect();
94 let entities = Entity::list_model(db, params, filter_values).await;
95 entities
96 }
97
98 async fn validate_entity(model: &mut ActixAdminModel, db: &DatabaseConnection) {
99 Entity::validate_model(model);
100
101 if !model.has_errors() {
102 let active_model = ActiveModel::from(model.clone());
103 let custom_errors = Entity::validate(&active_model);
104 model.custom_errors = custom_errors;
105 }
106
107 if model.has_errors() {
108 let mut model_entities = vec![model.clone()];
109 Self::load_foreign_keys(&mut model_entities, db).await;
110 model.fk_values = model_entities.pop().unwrap().fk_values;
111 }
112 }
113
114 async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel, tenant_ref: Option<i32>) -> Result<ActixAdminModel, ActixAdminError> {
115 Entity::validate_model(&mut model);
120 if model.has_errors() {
121 return Err(ActixAdminError {
122 ty: ActixAdminErrorType::ValidationErrors,
123 msg: "Model failed validation".to_string(),
124 });
125 }
126
127 let mut active_model = ActiveModel::from(model.clone());
128
129 #set_tenant_ref_field
130
131 let insert_operation = Entity::insert(active_model).exec(db).await?;
132 model.primary_key = Some(insert_operation.last_insert_id.to_string());
133
134 Ok(model)
135 }
136
137 async fn get_viewmodel_filter(db: &DatabaseConnection) -> HashMap<String, ActixAdminViewModelFilter> {
138 let mut hashmap: HashMap<String, ActixAdminViewModelFilter> = HashMap::new();
139
140 for filter in Entity::get_filter() {
141 hashmap.insert(
142 filter.name.to_string(),
143 ActixAdminViewModelFilter {
144 name: filter.name.to_string(),
145 value: None,
146 values: Entity::get_filter_values(&filter, db).await,
147 filter_type: Some(filter.filter_type),
148 foreign_key: filter.foreign_key.clone(),
149 operators: filter.operators.clone(),
150 operator: None,
151 }
152 );
153 };
154
155 hashmap
156 }
157
158 async fn get_entity(db: &DatabaseConnection, id: Self::Id, tenant_ref: Option<i32>) -> Result<ActixAdminModel, ActixAdminError> {
159 let mut query = Entity::find().filter(Column::#primary_key_column.eq(id));
160
161 #tenant_ref_field
162
163 let entity = query.one(db).await?;
164
165 match entity {
166 Some(e) => {
167 let model = ActixAdminModel::from(e);
168 let mut model_entities = Vec::<ActixAdminModel>::new();
169 model_entities.push(model);
170 let _load_fks = Self::load_foreign_keys(&mut model_entities, db).await;
171 Ok(model_entities.pop().unwrap())
172 },
173 _ => Err(ActixAdminError {
174 ty: ActixAdminErrorType::EntityDoesNotExistError,
175 msg: "".to_string()
176 })
177 }
178 }
179
180 async fn edit_entity(db: &DatabaseConnection, id: Self::Id, mut model: ActixAdminModel, tenant_ref: Option<i32>) -> Result<ActixAdminModel, ActixAdminError> {
181 Entity::validate_model(&mut model);
185 if model.has_errors() {
186 return Err(ActixAdminError {
187 ty: ActixAdminErrorType::ValidationErrors,
188 msg: "Model failed validation".to_string(),
189 });
190 }
191
192 let mut query = Entity::find().filter(Column::#primary_key_column.eq(id));
193
194 #tenant_ref_field
195
196 let entity = query.one(db).await?;
197
198 match entity {
199 Some(e) => {
200 let mut entity: ActiveModel = e.into();
201 #(#fields_for_edit_model);*;
202 let entity: Model = entity.update(db).await?;
203 Ok(model)
204 },
205 _ => Err(ActixAdminError {
206 ty: ActixAdminErrorType::EntityDoesNotExistError,
207 msg: "".to_string()
208 })
209 }
210 }
211
212 async fn delete_entity(db: &DatabaseConnection, id: Self::Id, tenant_ref: Option<i32>) -> Result<bool, ActixAdminError> {
213 let mut query = Entity::delete_many().filter(Column::#primary_key_column.eq(id));
214
215 #tenant_ref_field
216
217 let del_result = query.exec(db).await?;
218
219 if del_result.rows_affected > 0 {
220 return Ok(true)
221 } else {
222 return Err(ActixAdminError {
223 ty: ActixAdminErrorType::DeleteError,
224 msg: "".to_string()
225 })
226 }
227 }
228
229 async fn delete_entities(db: &DatabaseConnection, ids: &[Self::Id], tenant_ref: Option<i32>) -> Result<u64, ActixAdminError> {
230 if ids.is_empty() {
231 return Ok(0);
232 }
233 let mut query = Entity::delete_many()
234 .filter(Column::#primary_key_column.is_in(ids.iter().cloned()));
235
236 #tenant_ref_field
237
238 let del_result = query.exec(db).await?;
239 Ok(del_result.rows_affected)
240 }
241
242 async fn get_select_lists(db: &DatabaseConnection, tenant_ref: Option<i32>) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError> {
243 Ok(hashmap![
244 #(#select_lists),*
245 ])
246 }
247
248 fn get_entity_name() -> String {
249 Entity.table_name().to_string()
250 }
251 }
252 };
253
254 proc_macro::TokenStream::from(expanded)
255}
256
257#[proc_macro_derive(DeriveActixAdminModel, attributes(actix_admin))]
258pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
259 let fields = get_fields_for_tokenstream(input);
260
261 let field_names = get_fields_as_tokenstream(&fields, |model_field| -> String {
262 model_field.ident.to_string()
263 });
264 let field_html_input_type = get_fields_as_tokenstream(&fields, |model_field| -> String {
265 model_field.html_input_type.to_string()
266 });
267 let field_ceil = get_fields_as_opt_u8_tokens(&fields, |mf| {
268 mf.ceil.as_deref().and_then(|s| s.parse().ok())
269 });
270 let field_floor = get_fields_as_opt_u8_tokens(&fields, |mf| {
271 mf.floor.as_deref().and_then(|s| s.parse().ok())
272 });
273 let field_dateformat = get_fields_as_opt_string_tokens(&fields, |mf| {
274 let s = mf.dateformat.trim().to_string();
275 if s.is_empty() {
276 None
277 } else {
278 Some(s)
279 }
280 });
281 let field_shorten = get_fields_as_opt_u16_tokens(&fields, |mf| {
282 mf.shorten.as_deref().and_then(|s| s.parse().ok())
283 });
284 let field_foreign_key = get_fields_as_tokenstream(&fields, |model_field| -> String {
285 model_field.foreign_key.clone().unwrap_or("".to_string())
286 });
287 let field_list_regex_mask = get_fields_as_opt_string_tokens(&fields, |mf| {
288 if mf.list_regex_mask.is_empty() {
289 None
290 } else {
291 Some(mf.list_regex_mask.clone())
292 }
293 });
294 let field_select_list = get_fields_as_tokenstream(&fields, |model_field| -> String {
295 model_field.select_list.to_string()
296 });
297 let is_option_list =
298 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.is_option() });
299 let fields_for_create_model = get_fields_for_create_model(&fields);
300 let fields_for_from_model = get_fields_for_from_model(&fields);
301 let fields_for_load_foreign_key = get_fields_for_load_foreign_key(&fields);
302 let field_for_primary_key = get_field_for_primary_key(&fields);
303 let fields_for_validate_model = get_fields_for_validate_model(&fields);
304 let primary_key_column = get_primary_key_column_ident(&fields);
305 let fields_type_path = get_fields_as_tokenstream(&fields, |model_field| -> String {
306 model_field.get_type_path_string()
307 });
308 let fields_textarea =
309 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.textarea });
310 let fields_file_upload =
311 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.file_upload });
312 let fields_image =
313 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.image });
314 let fields_html_render =
315 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.html_render });
316 let fields_url = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.url });
317 let fields_email =
318 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.email });
319 let fields_wysiwyg =
320 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.wysiwyg });
321 let fields_readonly =
322 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.readonly });
323 let fields_match_name_to_columns = get_match_name_to_column(&fields);
324 let fields_list_sort_positions = get_fields_as_tokenstream(&fields, |model_field| -> usize {
325 model_field.list_sort_position
326 });
327 let fields_list_hide_column = get_fields_as_tokenstream(&fields, |model_field| -> bool {
328 model_field.list_hide_column
329 });
330 let fields_use_tom_select_callback =
331 get_fields_as_tokenstream(&fields, |model_field| -> bool {
332 model_field.use_tom_select_callback
333 });
334 let fields_tenant_ref =
335 get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.tenant_ref });
336 let fields_searchable = get_actix_admin_fields_searchable(&fields);
337 let has_searchable_fields = fields_searchable.len() > 0;
338 let tenant_ref_field = get_tenant_ref_field(&fields, true);
339
340 let expanded = quote! {
341 fn __actix_admin_viewmodel_fields() -> &'static [ActixAdminViewModelField] {
345 static FIELDS: ::std::sync::OnceLock<::std::vec::Vec<ActixAdminViewModelField>> =
346 ::std::sync::OnceLock::new();
347 FIELDS.get_or_init(|| {
348 let mut vec = Vec::new();
349
350 #(
351 let field_name: &str = #field_names;
352 let html_input_type: &str = #field_html_input_type;
353 let select_list: &str = #field_select_list;
354 let list_regex_mask_regex: Option<Regex> =
355 #field_list_regex_mask.map(|s: &str| Regex::new(s).unwrap());
356 let dateformat: Option<String> = #field_dateformat.map(|s: &str| s.to_string());
357 let ceil: Option<u8> = #field_ceil;
358 let floor: Option<u8> = #field_floor;
359 let shorten: Option<u16> = #field_shorten;
360
361 vec.push(ActixAdminViewModelField {
362 field_name: field_name.to_string(),
363 html_input_type: html_input_type.to_string(),
364 select_list: select_list.to_string(),
365 is_option: #is_option_list,
366 list_sort_position: #fields_list_sort_positions,
367 field_type: ActixAdminViewModelFieldType::get_field_type(
368 #fields_type_path,
369 select_list.to_string(),
370 #fields_textarea,
371 #fields_file_upload,
372 #fields_image,
373 #fields_html_render,
374 #fields_url,
375 #fields_email,
376 #fields_wysiwyg,
377 ),
378 list_hide_column: #fields_list_hide_column,
379 list_regex_mask: list_regex_mask_regex,
380 foreign_key: #field_foreign_key.to_string(),
381 is_tenant_ref: #fields_tenant_ref,
382 ceil: ceil,
383 floor: floor,
384 dateformat: dateformat,
385 shorten: shorten,
386 use_tom_select_callback: #fields_use_tom_select_callback,
387 readonly: #fields_readonly,
388 });
389 )*
390
391 vec
392 }).as_slice()
393 }
394
395 impl From<Model> for ActixAdminModel {
396 fn from(model: Model) -> Self {
397 let display_name = model.clone().to_string();
398 ActixAdminModel {
399 #field_for_primary_key,
400 values: hashmap![
401 #(#fields_for_from_model),*
402 ],
403 errors: HashMap::new(),
404 custom_errors: HashMap::new(),
405 fk_values: HashMap::new(),
406 display_name: Some(display_name)
407 }
408 }
409 }
410
411 impl From<ActixAdminModel> for ActiveModel {
412 fn from(model: ActixAdminModel) -> Self {
413 ActiveModel
414 {
415 #(#fields_for_create_model),*
416 ,
417 ..Default::default()
418 }
419 }
420 }
421
422 #[actix_admin::prelude::async_trait]
423 impl ActixAdminModelTrait for Entity {
424 async fn list_model(db: &DatabaseConnection, params: &ActixAdminViewModelParams, filter_values: HashMap<String, Option<String>>) -> Result<(Option<u64>, Vec<ActixAdminModel>), ActixAdminError> {
425
426 let filter_operators: HashMap<String, Option<actix_admin::prelude::ActixAdminFilterOperator>> = params.viewmodel_filter
427 .iter()
428 .map(|f| (f.name.clone(), f.operator.clone()))
429 .collect();
430
431 let sort_column = match params.sort_by.as_ref() {
432 #(#fields_match_name_to_columns)*
433 _ => Column::#primary_key_column,
438 };
439
440 let mut query = match params.sort_order {
441 SortOrder::Asc => Entity::find().order_by_asc(sort_column),
442 SortOrder::Desc => Entity::find().order_by_desc(sort_column),
443 };
444
445 if (#has_searchable_fields) {
446 query = query
447 .filter(
448 Condition::any()
449 #(#fields_searchable)*
450 )
451 }
452
453 #tenant_ref_field
454
455 let filters = Entity::get_filter();
456 for filter in filters {
457 let value = filter_values.get(&filter.name).unwrap_or_else(|| &None).clone();
458 let operator = filter_operators.get(&filter.name).cloned().flatten();
459 query = filter.filter.apply(query, value, operator);
460 }
461
462 let mut entities;
463 let mut model_entities = Vec::<ActixAdminModel>::new();
464 let num_pages: Option<u64>;
465
466 match (params.page, params.entities_per_page) {
467 (Some(p), Some(e)) => {
468 let paginator = query.paginate(db, e);
469 num_pages = Some(paginator.num_pages().await?);
470
471 if (num_pages.unwrap() == 0) { return Ok((num_pages, model_entities)) };
472 entities = paginator
476 .fetch_page(std::cmp::min(num_pages.unwrap() - 1, p.saturating_sub(1)))
477 .await?;
478 },
479 (_, _) => {
480 entities = query.all(db).await?;
481 num_pages = None;
482 }
483 };
484
485 for entity in entities {
486 model_entities.push(
487 ActixAdminModel::from(entity)
488 );
489 }
490
491 let _load_fks = Self::load_foreign_keys(&mut model_entities, db).await;
492
493 Ok((num_pages, model_entities))
494 }
495
496 async fn load_foreign_keys(models: &mut [ActixAdminModel], db: &DatabaseConnection) {
497 for field in Self::get_fields().iter() {
498 if field.foreign_key != "" {
499 let ids_to_select: Vec<i32> = models.iter()
500 .map(|m| m.values.get(&field.field_name))
501 .filter_map(|value| {
502 value.and_then(|s| s.parse().ok())
503 })
504 .collect();
505
506 let foreign_key_entity = field.foreign_key.trim_start_matches("'").trim_end_matches("'").replace('"', "").replace(' ', "").replace('\\', "").replace(' ', "").to_string();
507
508 let foreign_key_values_opt: Option<HashMap<String, String>> = match foreign_key_entity.as_str() {
509 #(#fields_for_load_foreign_key)*
510 _ => None
511 };
512
513 if foreign_key_values_opt.is_some() {
514 let foreign_key_values = foreign_key_values_opt.unwrap();
515 for model in models.iter_mut() {
516 let fk_id = model.values.get(&field.field_name).unwrap();
517 let fk_val = foreign_key_values.get(fk_id);
518 if fk_val.is_some() {
519 model.fk_values.insert(field.field_name.to_string(), fk_val.unwrap().to_string());
520 }
521 }
522 }
523 }
524 }
525 }
526
527 fn validate_model(model: &mut ActixAdminModel) {
528 let mut errors = HashMap::<String, String>::new();
529 #(#fields_for_validate_model);*
530
531 model.errors = errors;
532 }
533
534 fn get_fields() -> &'static[ActixAdminViewModelField] {
535 __actix_admin_viewmodel_fields()
536 }
537 }
538 };
539
540 proc_macro::TokenStream::from(expanded)
541}