crudcrate_derive/
lib.rs

1//! Procedural macros for generating CRUD operations from Sea-ORM entities.
2//!
3//! **Main macro**: `#[derive(EntityToModels)]` - see [`entity_to_models`]
4//!
5//! **Module guide**: `fields/` (field processing) | `codegen/` (models, handlers, joins, routes)
6
7mod attribute_parser;
8mod codegen;
9mod fields;
10mod macro_implementation;
11mod relation_validator;
12mod traits;
13
14use proc_macro::TokenStream;
15use quote::{format_ident, quote};
16use syn::{DeriveInput, parse_macro_input};
17use traits::crudresource::structs::CRUDResourceMeta;
18
19fn extract_active_model_type(
20    input: &DeriveInput,
21    name: &syn::Ident,
22) -> Result<proc_macro2::TokenStream, proc_macro2::TokenStream> {
23    for attr in &input.attrs {
24        if attr.path().is_ident("active_model")
25            && let Some(s) = attribute_parser::get_string_from_attr(attr)
26        {
27            return match syn::parse_str::<syn::Type>(&s) {
28                Ok(ty) => Ok(quote! { #ty }),
29                Err(_) => Err(syn::Error::new_spanned(
30                    attr,
31                    format!("Invalid active_model type: '{s}'. Expected a valid Rust type path."),
32                )
33                .to_compile_error()),
34            };
35        }
36    }
37    let ident = format_ident!("{}ActiveModel", name);
38    Ok(quote! { #ident })
39}
40
41
42/// Generates `<Name>Create` struct with fields not excluded by `exclude(create)`.
43/// Fields with `on_create` become `Option<T>` to allow user override.
44/// Implements `From<NameCreate>` for `ActiveModel` with automatic value generation.
45#[proc_macro_derive(ToCreateModel, attributes(crudcrate, active_model))]
46pub fn to_create_model(input: TokenStream) -> TokenStream {
47    let input = parse_macro_input!(input as DeriveInput);
48    let name = &input.ident;
49    let create_name = format_ident!("{}Create", name);
50
51    let active_model_type = match extract_active_model_type(&input, name) {
52        Ok(ty) => ty,
53        Err(e) => return e.into(),
54    };
55    let fields = match fields::extract_named_fields(&input) {
56        Ok(f) => f,
57        Err(e) => return e,
58    };
59    let create_struct_fields = codegen::models::create::generate_create_struct_fields(&fields);
60    let conv_lines = codegen::models::create::generate_create_conversion_lines(&fields);
61
62    // Always include ToSchema for Create models
63    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
64    let create_derives =
65        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
66
67    let expanded = quote! {
68        #[derive(#create_derives)]
69        pub struct #create_name {
70            #(#create_struct_fields),*
71        }
72
73        impl From<#create_name> for #active_model_type {
74            fn from(create: #create_name) -> Self {
75                #active_model_type {
76                    #(#conv_lines),*
77                }
78            }
79        }
80    };
81
82    TokenStream::from(expanded)
83}
84
85/// Generates `<Name>Update` struct with fields not excluded by `exclude(update)`.
86/// All fields are `Option<Option<T>>` to support partial updates and explicit null.
87/// Implements `MergeIntoActiveModel` trait with `on_update` expression handling.
88#[proc_macro_derive(ToUpdateModel, attributes(crudcrate, active_model))]
89pub fn to_update_model(input: TokenStream) -> TokenStream {
90    let input = parse_macro_input!(input as DeriveInput);
91    let name = &input.ident;
92    let update_name = format_ident!("{}Update", name);
93
94    let active_model_type = match extract_active_model_type(&input, name) {
95        Ok(ty) => ty,
96        Err(e) => return e.into(),
97    };
98    let fields = match fields::extract_named_fields(&input) {
99        Ok(f) => f,
100        Err(e) => return e,
101    };
102    let included_fields = crate::codegen::models::update::filter_update_fields(&fields);
103    let update_struct_fields =
104        crate::codegen::models::update::generate_update_struct_fields(&included_fields);
105    let included_merge = codegen::models::merge::generate_included_merge_code(&included_fields);
106    let excluded_merge = codegen::models::merge::generate_excluded_merge_code(&fields);
107
108    // Always include ToSchema for Update models
109    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
110    let update_derives =
111        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
112
113    let expanded = quote! {
114        #[derive(#update_derives)]
115        pub struct #update_name {
116            #(#update_struct_fields),*
117        }
118
119        impl #update_name {
120            pub fn merge_fields(self, mut model: #active_model_type) -> Result<#active_model_type, crudcrate::ApiError> {
121                #(#included_merge)*
122                #(#excluded_merge)*
123                Ok(model)
124            }
125        }
126
127        impl crudcrate::traits::MergeIntoActiveModel<#active_model_type> for #update_name {
128            fn merge_into_activemodel(self, model: #active_model_type) -> Result<#active_model_type, crudcrate::ApiError> {
129                Self::merge_fields(self, model)
130            }
131        }
132    };
133
134    TokenStream::from(expanded)
135}
136
137/// Generates `<Name>List` struct with fields not excluded by `exclude(list)`.
138/// Optimizes API payloads by excluding heavy fields (joins, large text) from list endpoints.
139/// Implements `From<Name>` and `From<Model>` conversions.
140#[proc_macro_derive(ToListModel, attributes(crudcrate))]
141pub fn to_list_model(input: TokenStream) -> TokenStream {
142    let input = parse_macro_input!(input as DeriveInput);
143    let name = &input.ident;
144    let list_name = format_ident!("{}List", name);
145
146    let fields = match fields::extract_named_fields(&input) {
147        Ok(f) => f,
148        Err(e) => return e,
149    };
150    let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&fields);
151    let list_from_assignments =
152        crate::codegen::models::list::generate_list_from_assignments(&fields);
153
154    // Always include ToSchema for List models
155    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
156    let list_derives = quote! { Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
157
158    let expanded = quote! {
159        #[derive(#list_derives)]
160        pub struct #list_name {
161            #(#list_struct_fields),*
162        }
163
164        impl From<#name> for #list_name {
165            fn from(model: #name) -> Self {
166                Self {
167                    #(#list_from_assignments),*
168                }
169            }
170        }
171    };
172
173    TokenStream::from(expanded)
174}
175
176/// Generates complete CRUD API structures from Sea-ORM entities.
177///
178/// Creates API struct, List/Response models, and `CRUDResource` implementation.
179/// Supports custom functions, joins, filtering, sorting, and fulltext search.
180///
181/// Key attributes: `api_struct`, `generate_router`, `exclude()`, `join()`, `on_create/update`.
182/// See crate documentation for full attribute reference and examples.
183/// # Panics
184///
185/// This function will panic in the following cases:
186/// - When deprecated syntax is used (e.g., `create_model = false` instead of `exclude(create)`)
187/// - When there are cyclic join dependencies without explicit depth specification
188/// - When required Sea-ORM relation enums are missing for join fields
189#[proc_macro_derive(EntityToModels, attributes(crudcrate))]
190pub fn entity_to_models(input: TokenStream) -> TokenStream {
191    let input = parse_macro_input!(input as DeriveInput);
192    let struct_name = &input.ident;
193
194    // Parse and validate attributes
195    let (api_struct_name, active_model_path) = fields::parse_entity_attributes(&input, struct_name);
196    let table_name = attribute_parser::extract_table_name(&input.attrs)
197        .unwrap_or_else(|| struct_name.to_string());
198    let meta = attribute_parser::parse_crud_resource_meta(&input.attrs);
199
200    // Check for deprecation errors (legacy fn_* syntax)
201    if !meta.deprecation_errors.is_empty() {
202        let errors: proc_macro2::TokenStream = meta
203            .deprecation_errors
204            .iter()
205            .map(syn::Error::to_compile_error)
206            .collect();
207        return errors.into();
208    }
209
210    let crud_meta = meta.with_defaults(&table_name);
211
212    // Validate active model path
213    if syn::parse_str::<syn::Type>(&active_model_path).is_err() {
214        return syn::Error::new_spanned(
215            &input,
216            format!("Invalid active_model path: {active_model_path}"),
217        )
218        .to_compile_error()
219        .into();
220    }
221
222    // Extract fields and create field analysis
223    let fields = match fields::extract_entity_fields(&input) {
224        Ok(f) => f,
225        Err(e) => return e,
226    };
227    let field_analysis = match fields::analyze_entity_fields(fields) {
228        Ok(a) => a,
229        Err(e) => return e,
230    };
231    if let Err(e) = fields::validate_field_analysis(&field_analysis) {
232        return e;
233    }
234
235    // Setup join validation - check for cyclic dependencies
236    let cyclic_dependency_check = relation_validator::generate_cyclic_dependency_check(
237        &field_analysis,
238        &api_struct_name.to_string(),
239    );
240    if !cyclic_dependency_check.is_empty() {
241        return cyclic_dependency_check.into();
242    }
243
244    // Generate core API model components
245    let (api_struct_fields, from_model_assignments) =
246        codegen::models::api_struct::generate_api_struct_content(&field_analysis, &api_struct_name);
247    let api_struct = codegen::models::api_struct::generate_api_struct(
248        &api_struct_name,
249        &api_struct_fields,
250        &active_model_path,
251        &crud_meta,
252        &field_analysis,
253    );
254    let from_impl = quote! {
255        impl From<#struct_name> for #api_struct_name {
256            fn from(model: #struct_name) -> Self {
257                Self {
258                    #(#from_model_assignments),*
259                }
260            }
261        }
262    };
263
264    // Generate CRUD implementation
265    let has_crud_resource_fields = field_analysis.primary_key_field.is_some()
266        || !field_analysis.sortable_fields.is_empty()
267        || !field_analysis.filterable_fields.is_empty()
268        || !field_analysis.fulltext_fields.is_empty();
269
270    let crud_impl_inner = if has_crud_resource_fields {
271        macro_implementation::generate_crud_resource_impl(
272            &api_struct_name,
273            &crud_meta,
274            &active_model_path,
275            &field_analysis,
276            &table_name,
277        )
278    } else {
279        quote! {}
280    };
281
282    let router_impl = if crud_meta.generate_router && has_crud_resource_fields {
283        crate::codegen::router::axum::generate_router_impl(&api_struct_name)
284    } else {
285        quote! {}
286    };
287
288    let crud_impl = quote! {
289        #crud_impl_inner
290        #router_impl
291    };
292
293    // Generate list and response models
294    let (list_model, response_model) =
295        codegen::models::list_response::generate_list_and_response_models(
296            &input,
297            &api_struct_name,
298            struct_name,
299            &field_analysis,
300        );
301
302    // Generate final output
303    let expanded = quote! {
304        #api_struct
305        #from_impl
306        #crud_impl
307        #list_model
308        #response_model
309    };
310
311    TokenStream::from(expanded)
312}