Skip to main content

actix_admin_macros/
lib.rs

1//! # Actix Admin Macros
2//!
3//! Macros used by the actix-admin crate
4
5use 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                // Guard the infallible `ActiveModel::from` conversion below,
116                // which `.unwrap()`s each field. The route layer already
117                // validates, but a direct caller could pass a malformed model;
118                // fail with a validation error instead of panicking.
119                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                // Guard the `.unwrap()`-based field assignments below against a
182                // malformed model passed by a direct caller that skipped the
183                // route-layer validation.
184                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        // Lazily-initialized static list of the entity's fields, populated on
342        // first access. Uses `std::sync::OnceLock` instead of `lazy_static` so
343        // there is no runtime crate dependency for statics.
344        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                    // Fallback: the route layer validates `sort_by` before
434                    // reaching us via `validate_sort_by`, so this arm is only
435                    // reachable if a custom caller bypassed validation. Sort
436                    // by the primary key instead of panicking.
437                    _ => 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                        // `p` is 1-based; guard against a 0 slipping through a
473                        // custom caller that bypassed the route-layer clamp so
474                        // `p - 1` cannot underflow.
475                        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}