crudcrate_derive/
lib.rs

1mod attribute_parser;
2mod attributes;
3mod codegen;
4mod field_analyzer;
5mod macro_implementation;
6mod relation_validator;
7mod traits;
8
9use crate::codegen::join_strategies::get_join_config;
10use heck::ToPascalCase;
11use proc_macro::TokenStream;
12use quote::{ToTokens, format_ident, quote};
13use syn::{
14    Data, DeriveInput, Fields, Lit, Meta, parse::Parser, parse_macro_input, punctuated::Punctuated,
15    token::Comma,
16};
17use traits::crudresource::structs::{CRUDResourceMeta, EntityFieldAnalysis};
18
19fn extract_active_model_type(input: &DeriveInput, name: &syn::Ident) -> proc_macro2::TokenStream {
20    let mut active_model_override = None;
21    for attr in &input.attrs {
22        if attr.path().is_ident("active_model")
23            && let Some(s) = attribute_parser::get_string_from_attr(attr)
24        {
25            active_model_override =
26                Some(syn::parse_str::<syn::Type>(&s).expect("Invalid active_model type"));
27        }
28    }
29    if let Some(ty) = active_model_override {
30        quote! { #ty }
31    } else {
32        let ident = format_ident!("{}ActiveModel", name);
33        quote! { #ident }
34    }
35}
36
37fn extract_named_fields(
38    input: &DeriveInput,
39) -> syn::punctuated::Punctuated<syn::Field, syn::token::Comma> {
40    if let Data::Struct(data) = &input.data {
41        if let Fields::Named(named) = &data.fields {
42            named.named.clone()
43        } else {
44            panic!("ToCreateModel only supports structs with named fields");
45        }
46    } else {
47        panic!("ToCreateModel can only be derived for structs");
48    }
49}
50
51fn generate_update_merge_code(
52    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
53    included_fields: &[&syn::Field],
54) -> (Vec<proc_macro2::TokenStream>, Vec<proc_macro2::TokenStream>) {
55    let included_merge = generate_included_merge_code(included_fields);
56    let excluded_merge = generate_excluded_merge_code(fields);
57    (included_merge, excluded_merge)
58}
59
60fn generate_included_merge_code(included_fields: &[&syn::Field]) -> Vec<proc_macro2::TokenStream> {
61    included_fields
62        .iter()
63        .filter(|field| {
64            !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
65        })
66        .map(|field| {
67            let ident = &field.ident;
68            let is_optional = field_analyzer::field_is_optional(field);
69
70            if is_optional {
71                quote! {
72                    model.#ident = match self.#ident {
73                        Some(Some(value)) => sea_orm::ActiveValue::Set(Some(value.into())),
74                        Some(None)      => sea_orm::ActiveValue::Set(None),
75                        None            => sea_orm::ActiveValue::NotSet,
76                    };
77                }
78            } else {
79                quote! {
80                    model.#ident = match self.#ident {
81                        Some(Some(value)) => sea_orm::ActiveValue::Set(value.into()),
82                        Some(None) => {
83                            return Err(sea_orm::DbErr::Custom(format!(
84                                "Field '{}' is required and cannot be set to null",
85                                stringify!(#ident)
86                            )));
87                        },
88                        None => sea_orm::ActiveValue::NotSet,
89                    };
90                }
91            }
92        })
93        .collect()
94}
95
96fn generate_excluded_merge_code(
97    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
98) -> Vec<proc_macro2::TokenStream> {
99    fields
100        .iter()
101        .filter(|field| {
102            attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
103                && !attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false)
104        })
105        .filter_map(|field| {
106            if let Some(expr) = attribute_parser::get_crudcrate_expr(field, "on_update") {
107                let ident = &field.ident;
108                if field_analyzer::field_is_optional(field) {
109                    Some(quote! {
110                        model.#ident = sea_orm::ActiveValue::Set(Some((#expr).into()));
111                    })
112                } else {
113                    Some(quote! {
114                        model.#ident = sea_orm::ActiveValue::Set((#expr).into());
115                    })
116                }
117            } else {
118                None
119            }
120        })
121        .collect()
122}
123
124fn extract_entity_fields(
125    input: &DeriveInput,
126) -> Result<&syn::punctuated::Punctuated<syn::Field, syn::token::Comma>, TokenStream> {
127    match &input.data {
128        Data::Struct(data) => match &data.fields {
129            Fields::Named(fields) => Ok(&fields.named),
130            _ => Err(syn::Error::new_spanned(
131                input,
132                "EntityToModels only supports structs with named fields",
133            )
134            .to_compile_error()
135            .into()),
136        },
137        _ => Err(
138            syn::Error::new_spanned(input, "EntityToModels only supports structs")
139                .to_compile_error()
140                .into(),
141        ),
142    }
143}
144
145fn parse_entity_attributes(input: &DeriveInput, struct_name: &syn::Ident) -> (syn::Ident, String) {
146    let mut api_struct_name = None;
147    let mut active_model_path = None;
148
149    for attr in &input.attrs {
150        if attr.path().is_ident("crudcrate")
151            && let Meta::List(meta_list) = &attr.meta
152            && let Ok(metas) =
153                Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
154        {
155            for meta in &metas {
156                if let Meta::NameValue(nv) = meta {
157                    if nv.path.is_ident("api_struct") {
158                        if let syn::Expr::Lit(expr_lit) = &nv.value
159                            && let Lit::Str(s) = &expr_lit.lit
160                        {
161                            api_struct_name = Some(format_ident!("{}", s.value()));
162                        }
163                    } else if nv.path.is_ident("active_model")
164                        && let syn::Expr::Lit(expr_lit) = &nv.value
165                        && let Lit::Str(s) = &expr_lit.lit
166                    {
167                        active_model_path = Some(s.value());
168                    }
169                }
170            }
171        }
172    }
173
174    let table_name = attribute_parser::extract_table_name(&input.attrs)
175        .unwrap_or_else(|| struct_name.to_string());
176    let api_struct_name =
177        api_struct_name.unwrap_or_else(|| format_ident!("{}", table_name.to_pascal_case()));
178    let active_model_path = active_model_path.unwrap_or_else(|| "ActiveModel".to_string());
179
180    (api_struct_name, active_model_path)
181}
182
183fn analyze_entity_fields(
184    fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
185) -> EntityFieldAnalysis<'_> {
186    let mut analysis = EntityFieldAnalysis {
187        db_fields: Vec::new(),
188        non_db_fields: Vec::new(),
189        primary_key_field: None,
190        sortable_fields: Vec::new(),
191        filterable_fields: Vec::new(),
192        fulltext_fields: Vec::new(),
193        join_on_one_fields: Vec::new(),
194        join_on_all_fields: Vec::new(),
195        // join_configs: std::collections::HashMap::new(), // Removed due to HashMap key issues
196    };
197
198    for field in fields {
199        let is_non_db = attribute_parser::get_crudcrate_bool(field, "non_db_attr").unwrap_or(false);
200
201        // Check for join attributes regardless of db/non_db status
202        if let Some(join_config) = get_join_config(field) {
203            if join_config.on_one {
204                analysis.join_on_one_fields.push(field);
205            }
206            if join_config.on_all {
207                analysis.join_on_all_fields.push(field);
208            }
209            // Note: join_configs removed to avoid HashMap key issues with syn::Field
210        }
211
212        if is_non_db {
213            analysis.non_db_fields.push(field);
214        } else {
215            analysis.db_fields.push(field);
216
217            if attribute_parser::field_has_crudcrate_flag(field, "primary_key") {
218                analysis.primary_key_field = Some(field);
219            }
220            if attribute_parser::field_has_crudcrate_flag(field, "sortable") {
221                analysis.sortable_fields.push(field);
222            }
223            if attribute_parser::field_has_crudcrate_flag(field, "filterable") {
224                analysis.filterable_fields.push(field);
225            }
226            if attribute_parser::field_has_crudcrate_flag(field, "fulltext") {
227                analysis.fulltext_fields.push(field);
228            }
229        }
230    }
231
232    analysis
233}
234
235fn validate_field_analysis(analysis: &EntityFieldAnalysis) -> Result<(), TokenStream> {
236    if analysis.primary_key_field.is_some()
237        && analysis
238            .db_fields
239            .iter()
240            .filter(|field| attribute_parser::field_has_crudcrate_flag(field, "primary_key"))
241            .count()
242            > 1
243    {
244        return Err(syn::Error::new_spanned(
245            analysis.primary_key_field.unwrap(),
246            "Only one field can be marked with 'primary_key' attribute",
247        )
248        .to_compile_error()
249        .into());
250    }
251
252    // Validate that non_db_attr fields have #[sea_orm(ignore)]
253    for field in &analysis.non_db_fields {
254        if !has_sea_orm_ignore(field) {
255            let field_name = field
256                .ident
257                .as_ref()
258                .map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
259            return Err(syn::Error::new_spanned(
260                field,
261                format!(
262                    "Field '{field_name}' has #[crudcrate(non_db_attr)] but is missing #[sea_orm(ignore)].\n\
263                     Non-database fields must be marked with both attributes.\n\
264                     Add #[sea_orm(ignore)] above the #[crudcrate(...)] attribute."
265                ),
266            )
267            .to_compile_error()
268            .into());
269        }
270    }
271
272    Ok(())
273}
274
275/// Check if a field has the `#[sea_orm(ignore)]` attribute
276fn has_sea_orm_ignore(field: &syn::Field) -> bool {
277    for attr in &field.attrs {
278        if attr.path().is_ident("sea_orm")
279            && let Meta::List(meta_list) = &attr.meta
280            && let Ok(metas) =
281                Punctuated::<Meta, Comma>::parse_terminated.parse2(meta_list.tokens.clone())
282        {
283            for meta in metas {
284                if let Meta::Path(path) = meta
285                    && path.is_ident("ignore")
286                {
287                    return true;
288                }
289            }
290        }
291    }
292    false
293}
294
295fn resolve_join_field_type_preserving_container(
296    field_type: &syn::Type,
297) -> proc_macro2::TokenStream {
298    // For join fields, use the type exactly as written by the user
299    // This allows any valid Rust path without making assumptions
300    quote! { #field_type }
301}
302
303fn generate_api_struct_content(
304    analysis: &EntityFieldAnalysis,
305    _api_struct_name: &syn::Ident,
306) -> (
307    Vec<proc_macro2::TokenStream>,
308    Vec<proc_macro2::TokenStream>,
309    std::collections::HashSet<String>,
310) {
311    let mut api_struct_fields = Vec::new();
312    let mut from_model_assignments = Vec::new();
313    let required_imports = std::collections::HashSet::new();
314
315    for field in &analysis.db_fields {
316        let field_name = &field.ident;
317        let field_type = &field.ty;
318
319        let api_field_attrs: Vec<_> = field
320            .attrs
321            .iter()
322            .filter(|attr| !attr.path().is_ident("sea_orm"))
323            .collect();
324
325        api_struct_fields.push(quote! {
326            #(#api_field_attrs)*
327            pub #field_name: #field_type
328        });
329
330        // Also populate the From<Model> assignment for this field (since it exists in the struct)
331        let assignment = if field_type
332            .to_token_stream()
333            .to_string()
334            .contains("DateTimeWithTimeZone")
335        {
336            if field_analyzer::field_is_optional(field) {
337                quote! {
338                    #field_name: model.#field_name.map(|dt| dt.with_timezone(&chrono::Utc))
339                }
340            } else {
341                quote! {
342                    #field_name: model.#field_name.with_timezone(&chrono::Utc)
343                }
344            }
345        } else {
346            quote! {
347                #field_name: model.#field_name
348            }
349        };
350
351        from_model_assignments.push(assignment);
352    }
353
354    for field in &analysis.non_db_fields {
355        let field_name = &field.ident;
356        let field_type = &field.ty;
357
358        let default_expr = attribute_parser::get_crudcrate_expr(field, "default")
359            .unwrap_or_else(|| syn::parse_quote!(Default::default()));
360
361        // Preserve all original crudcrate attributes while ensuring required ones are present
362        let crudcrate_attrs: Vec<_> = field
363            .attrs
364            .iter()
365            .filter(|attr| attr.path().is_ident("crudcrate"))
366            .collect();
367
368        // Add schema(no_recursion) attribute for join fields to prevent circular dependencies
369        // This is the proper utoipa way to handle recursive relationships
370        let schema_attrs = if get_join_config(field).is_some() {
371            quote! { #[schema(no_recursion)] }
372        } else {
373            quote! {}
374        };
375
376        let final_field_type = if get_join_config(field).is_some() {
377            resolve_join_field_type_preserving_container(field_type)
378        } else {
379            quote! { #field_type }
380        };
381
382        let field_definition = quote! {
383            #schema_attrs
384            #(#crudcrate_attrs)*
385            pub #field_name: #final_field_type
386        };
387
388        api_struct_fields.push(field_definition);
389
390        let assignment = if get_join_config(field).is_some() {
391            let empty_value = if let Ok(syn::Type::Path(type_path)) =
392                syn::parse2::<syn::Type>(quote! { #final_field_type })
393            {
394                if let Some(segment) = type_path.path.segments.last() {
395                    if segment.ident == "Vec" {
396                        quote! { vec![] }
397                    } else if segment.ident == "Option" {
398                        quote! { None }
399                    } else {
400                        quote! { Default::default() }
401                    }
402                } else {
403                    quote! { Default::default() }
404                }
405            } else {
406                quote! { Default::default() }
407            };
408
409            quote! {
410                #field_name: #empty_value
411            }
412        } else {
413            quote! {
414                #field_name: #default_expr
415            }
416        };
417
418        from_model_assignments.push(assignment);
419    }
420
421    (api_struct_fields, from_model_assignments, required_imports)
422}
423
424fn generate_api_struct(
425    api_struct_name: &syn::Ident,
426    api_struct_fields: &[proc_macro2::TokenStream],
427    active_model_path: &str,
428    crud_meta: &crate::traits::crudresource::structs::CRUDResourceMeta,
429    analysis: &EntityFieldAnalysis,
430    _required_imports: &std::collections::HashSet<String>,
431) -> proc_macro2::TokenStream {
432    // Check if we have fields excluded from create/update models
433    let _has_create_exclusions = analysis
434        .db_fields
435        .iter()
436        .chain(analysis.non_db_fields.iter())
437        .any(|field| attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false));
438    let _has_update_exclusions = analysis
439        .db_fields
440        .iter()
441        .chain(analysis.non_db_fields.iter())
442        .any(|field| attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false));
443
444    // Check if we have join fields that require Default implementation
445    let has_join_fields =
446        !analysis.join_on_one_fields.is_empty() || !analysis.join_on_all_fields.is_empty();
447
448    // Check if any non-db fields need Default (for join loading or excluded fields)
449    let has_fields_needing_default = has_join_fields
450        || analysis.non_db_fields.iter().any(|field| {
451            // Fields excluded from create/update need Default for join loading
452            attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
453                || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
454        })
455        || analysis.db_fields.iter().any(|field| {
456            // Database fields excluded from create/update need Default
457            attribute_parser::get_crudcrate_bool(field, "create_model") == Some(false)
458                || attribute_parser::get_crudcrate_bool(field, "update_model") == Some(false)
459        });
460
461    // Build derive clause based on user preferences
462    let mut derives = vec![
463        quote!(Clone),
464        quote!(Debug),
465        quote!(Serialize),
466        quote!(Deserialize),
467        quote!(ToCreateModel),
468        quote!(ToUpdateModel),
469    ];
470
471    // Always include ToSchema, but handle circular dependencies with schema(no_recursion)
472    // This is the proper utoipa approach for recursive relationships
473    derives.push(quote!(ToSchema));
474
475    // Add Default derive if needed for join fields or excluded fields
476    // BUT: don't derive Default if we have join fields, as it causes E0282 type inference errors
477    // We'll manually implement Default instead
478    if has_fields_needing_default && !has_join_fields {
479        derives.push(quote!(Default));
480    }
481
482    if crud_meta.derive_partial_eq {
483        derives.push(quote!(PartialEq));
484    }
485
486    if crud_meta.derive_eq {
487        derives.push(quote!(Eq));
488    }
489
490    // No import generation needed - types are used as-written
491
492    quote! {
493        use sea_orm::ActiveValue;
494        use utoipa::ToSchema;
495        use serde::{Serialize, Deserialize};
496        use crudcrate::{ToUpdateModel, ToCreateModel};
497
498        #[derive(#(#derives),*)]
499        #[active_model = #active_model_path]
500        pub struct #api_struct_name {
501            #(#api_struct_fields),*
502        }
503    }
504}
505
506fn generate_from_impl(
507    struct_name: &syn::Ident,
508    api_struct_name: &syn::Ident,
509    from_model_assignments: &[proc_macro2::TokenStream],
510) -> proc_macro2::TokenStream {
511    quote! {
512        impl From<#struct_name> for #api_struct_name {
513            fn from(model: #struct_name) -> Self {
514                Self {
515                    #(#from_model_assignments),*
516                }
517            }
518        }
519    }
520}
521
522fn generate_conditional_crud_impl(
523    api_struct_name: &syn::Ident,
524    crud_meta: &CRUDResourceMeta,
525    active_model_path: &str,
526    analysis: &EntityFieldAnalysis,
527    table_name: &str,
528) -> proc_macro2::TokenStream {
529    // Note: join fields should not be considered for CRUD resource generation
530    // They are populated by join loading, not direct CRUD operations
531    let has_crud_resource_fields = analysis.primary_key_field.is_some()
532        || !analysis.sortable_fields.is_empty()
533        || !analysis.filterable_fields.is_empty()
534        || !analysis.fulltext_fields.is_empty();
535
536    let crud_impl = if has_crud_resource_fields {
537        macro_implementation::generate_crud_resource_impl(
538            api_struct_name,
539            crud_meta,
540            active_model_path,
541            analysis,
542            table_name,
543        )
544    } else {
545        quote! {}
546    };
547
548    let router_impl = if crud_meta.generate_router && has_crud_resource_fields {
549        crate::codegen::router::axum::generate_router_impl(api_struct_name)
550    } else {
551        quote! {}
552    };
553
554    quote! {
555        #crud_impl
556        #router_impl
557    }
558}
559
560/// ===================
561/// `ToCreateModel` Macro
562/// ===================
563/// This macro:
564/// 1. Generates a struct named `<OriginalName>Create` that includes only the fields
565///    where `#[crudcrate(create_model = false)]` is NOT specified (default = true).
566///    If a field has an `on_create` expression, its type becomes `Option<…>`
567///    (with `#[serde(default)]`) so the user can override that default.
568/// 2. Generates an `impl From<<OriginalName>Create> for <ActiveModelType>>` where:
569///    - For each field with `on_create`:
570///       - If the original type was `Option<T>`, then `create.<field>` is `Option<Option<T>>`.
571///         We match on that and do:
572///           ```rust,ignore
573///           match create.field {
574///             Some(Some(v)) => Some(v.into()),      // user overrode with T
575///             Some(None)    => None,                // user explicitly set null
576///             None          => Some((expr).into()), // fallback to expr
577///           }
578///           ```
579///       - If the original type was non‐optional `T`, then `create.<field>` is `Option<T>`.
580///         We match on that and do:
581///           ```rust,ignore
582///           match create.field {
583///             Some(v) => v.into(),
584///             None    => (expr).into(),
585///           }
586///           ```
587///    - For each field without `on_create`:
588///       - If the original type was `Option<T>`, we do `create.<field>.map(|v| v.into())`.
589///       - If it was non‐optional `T`, we do `create.<field>.into()`.
590///    - For any field excluded (`create_model = false`) but having `on_create`, we do
591///      `Some((expr).into())` if it was `Option<T>`, or just `(expr).into()` otherwise.
592#[proc_macro_derive(ToCreateModel, attributes(crudcrate, active_model))]
593pub fn to_create_model(input: TokenStream) -> TokenStream {
594    let input = parse_macro_input!(input as DeriveInput);
595    let name = &input.ident;
596    let create_name = format_ident!("{}Create", name);
597
598    let active_model_type = extract_active_model_type(&input, name);
599    let fields = extract_named_fields(&input);
600    let create_struct_fields = codegen::models::create::generate_create_struct_fields(&fields);
601    let conv_lines = codegen::models::create::generate_create_conversion_lines(&fields);
602
603    // Always include ToSchema for Create models
604    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
605    let create_derives =
606        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
607
608    let expanded = quote! {
609        #[derive(#create_derives)]
610        pub struct #create_name {
611            #(#create_struct_fields),*
612        }
613
614        impl From<#create_name> for #active_model_type {
615            fn from(create: #create_name) -> Self {
616                #active_model_type {
617                    #(#conv_lines),*
618                }
619            }
620        }
621    };
622
623    TokenStream::from(expanded)
624}
625
626/// ===================
627/// `ToUpdateModel` Macro
628/// ===================
629/// This macro:
630/// 1. Generates a struct named `<OriginalName>Update` that includes only the fields
631///    where `#[crudcrate(update_model = false)]` is NOT specified (default = true).
632/// 2. Generates an impl for a method
633///    `merge_into_activemodel(self, mut model: ActiveModelType) -> ActiveModelType`
634///    that, for each field:
635///    - If it's included in the update struct, and the user provided a value:
636///       - If the original field type was `Option<T>`, we match on
637///         `Option<Option<T>>`:
638///           ```rust,ignore
639///           Some(Some(v)) => ActiveValue::Set(Some(v.into())),
640///           Some(None)    => ActiveValue::Set(None),     // explicit set to None
641///           None          => ActiveValue::NotSet,       // no change
642///           ```
643///       - If the original field type was non‐optional `T`, we match on `Option<T>`:
644///           ```rust,ignore
645///           Some(val) => ActiveValue::Set(val.into()),
646///           _         => ActiveValue::NotSet,
647///           ```
648///    - If it's excluded (`update_model = false`) but has `on_update = expr`, we do
649///      `ActiveValue::Set(expr.into())` (wrapped in `Some(...)` if the original field was `Option<T>`).
650///    - All other fields remain unchanged.
651#[proc_macro_derive(ToUpdateModel, attributes(crudcrate, active_model))]
652pub fn to_update_model(input: TokenStream) -> TokenStream {
653    let input = parse_macro_input!(input as DeriveInput);
654    let name = &input.ident;
655    let update_name = format_ident!("{}Update", name);
656
657    let active_model_type = extract_active_model_type(&input, name);
658    let fields = extract_named_fields(&input);
659    let included_fields = crate::codegen::models::update::filter_update_fields(&fields);
660    let update_struct_fields =
661        crate::codegen::models::update::generate_update_struct_fields(&included_fields);
662    let (included_merge, excluded_merge) = generate_update_merge_code(&fields, &included_fields);
663
664    // Always include ToSchema for Update models
665    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
666    let update_derives =
667        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
668
669    let expanded = quote! {
670        #[derive(#update_derives)]
671        pub struct #update_name {
672            #(#update_struct_fields),*
673        }
674
675        impl #update_name {
676            pub fn merge_fields(self, mut model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
677                #(#included_merge)*
678                #(#excluded_merge)*
679                Ok(model)
680            }
681        }
682
683        impl crudcrate::traits::MergeIntoActiveModel<#active_model_type> for #update_name {
684            fn merge_into_activemodel(self, model: #active_model_type) -> Result<#active_model_type, sea_orm::DbErr> {
685                Self::merge_fields(self, model)
686            }
687        }
688    };
689
690    TokenStream::from(expanded)
691}
692
693/// ===================
694/// `ToListModel` Macro
695/// ===================
696/// This macro generates a struct named `<OriginalName>List` that includes only the fields
697/// where `#[crudcrate(list_model = false)]` is NOT specified (default = true).
698/// This allows creating optimized list views by excluding heavy fields like relationships,
699/// large text fields, or computed properties from collection endpoints.
700///
701/// Generated struct:
702/// ```rust,ignore
703/// #[derive(Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
704/// pub struct <OriginalName>List {
705///     // All fields where list_model != false
706///     pub field_name: FieldType,
707/// }
708///
709/// impl From<Model> for <OriginalName>List {
710///     fn from(model: Model) -> Self {
711///         Self {
712///             field_name: model.field_name,
713///             // ... other included fields
714///         }
715///     }
716/// }
717/// ```
718///
719/// Usage:
720/// ```rust,ignore
721/// pub struct Model {
722///     pub id: Uuid,
723///     pub name: String,
724///     #[crudcrate(list_model = false)]  // Exclude from list view
725///     pub large_description: Option<String>,
726///     #[crudcrate(list_model = false)]  // Exclude relationships from list
727///     pub related_items: Vec<RelatedItem>,
728/// }
729/// ```
730#[proc_macro_derive(ToListModel, attributes(crudcrate))]
731pub fn to_list_model(input: TokenStream) -> TokenStream {
732    let input = parse_macro_input!(input as DeriveInput);
733    let name = &input.ident;
734    let list_name = format_ident!("{}List", name);
735
736    let fields = extract_named_fields(&input);
737    let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&fields);
738    let list_from_assignments =
739        crate::codegen::models::list::generate_list_from_assignments(&fields);
740
741    // Always include ToSchema for List models
742    // Circular dependencies are handled by schema(no_recursion) on join fields in the main model
743    let list_derives = quote! { Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
744
745    let expanded = quote! {
746        #[derive(#list_derives)]
747        pub struct #list_name {
748            #(#list_struct_fields),*
749        }
750
751        impl From<#name> for #list_name {
752            fn from(model: #name) -> Self {
753                Self {
754                    #(#list_from_assignments),*
755                }
756            }
757        }
758    };
759
760    TokenStream::from(expanded)
761}
762
763/// =====================
764/// `EntityToModels` Macro
765/// =====================
766/// This macro generates an API struct from a Sea-ORM entity Model struct, along with
767/// `ToCreateModel` and `ToUpdateModel` implementations.
768///
769/// ## Available Struct-Level Attributes
770///
771/// ```rust,ignore
772/// #[crudcrate(
773///     api_struct = "TodoItem",              // Override API struct name
774///     active_model = "ActiveModel",         // Override ActiveModel path
775///     name_singular = "todo",               // Resource name (singular)
776///     name_plural = "todos",                // Resource name (plural)
777///     description = "Manages todo items",   // Resource description
778///     entity_type = "Entity",               // Entity type for CRUDResource
779///     column_type = "Column",               // Column type for CRUDResource
780///     fn_get_one = self::custom_get_one,    // Custom get_one function
781///     fn_get_all = self::custom_get_all,    // Custom get_all function
782///     fn_create = self::custom_create,      // Custom create function
783///     fn_update = self::custom_update,      // Custom update function
784///     fn_delete = self::custom_delete,      // Custom delete function
785///     fn_delete_many = self::custom_delete_many, // Custom delete_many function
786/// )]
787/// ```
788///
789/// ## Available Field-Level Attributes
790///
791/// ```rust,ignore
792/// #[crudcrate(
793///     primary_key,                          // Mark as primary key
794///     sortable,                             // Include in sortable columns
795///     filterable,                           // Include in filterable columns
796///     create_model = false,                 // Exclude from Create model
797///     update_model = false,                 // Exclude from Update model
798///     on_create = Uuid::new_v4(),          // Auto-generate on create
799///     on_update = chrono::Utc::now(),      // Auto-update on update
800///     non_db_attr = true,                  // Non-database field
801///     default = vec![],                    // Default for non-DB fields
802///     use_target_models,                   // Use target's Create/Update models for relationships
803/// )]
804/// ```
805///
806/// Usage:
807/// ```ignore
808/// use uuid::Uuid;
809/// #[derive(EntityToModels)]
810/// #[crudcrate(api_struct = "Experiment", active_model = "spice_entity::experiments::ActiveModel")]
811/// pub struct Model {
812///     #[crudcrate(update_model = false, create_model = false, on_create = Uuid::new_v4())]
813///     pub id: Uuid,
814///     pub name: String,
815///     #[crudcrate(non_db_attr = true, default = vec![])]
816///     pub regions: Vec<RegionInput>,
817/// }
818/// ```
819///
820/// This generates:
821/// - An API struct with the specified name (e.g., `EntityName`)
822/// - `ToCreateModel` and `ToUpdateModel` implementations
823/// - `From<Model>` implementation for the API struct
824/// - Support for non-db attributes
825///
826/// Derive macro for generating complete CRUD API structures from Sea-ORM entities.
827///
828/// # Struct-Level Attributes (all optional)
829///
830/// **Boolean Flags** (can be used as just `flag` or `flag = true/false`):
831/// - `generate_router` - Auto-generate Axum router with all CRUD endpoints
832/// - `debug_output` - Print generated code to console (requires `--features debug`)
833///
834/// **Named Parameters**:
835/// - `api_struct = "Name"` - Override API struct name (default: table name in `PascalCase`)
836/// - `active_model = "Path"` - Override `ActiveModel` path (default: `ActiveModel`)
837/// - `name_singular = "name"` - Resource singular name (default: table name)
838/// - `name_plural = "names"` - Resource plural name (default: singular + "s")
839/// - `description = "desc"` - Resource description for documentation
840/// - `entity_type = "Entity"` - Entity type for `CRUDResource` (default: "Entity")
841/// - `column_type = "Column"` - Column type for `CRUDResource` (default: "Column")
842/// - `fulltext_language = "english"` - Default language for full-text search
843///
844/// **Function Overrides** (for custom CRUD behavior):
845/// - `fn_get_one = path::to::function` - Custom `get_one` function override
846/// - `fn_get_all = path::to::function` - Custom `get_all` function override
847/// - `fn_create = path::to::function` - Custom create function override
848/// - `fn_update = path::to::function` - Custom update function override
849/// - `fn_delete = path::to::function` - Custom delete function override
850/// - `fn_delete_many = path::to::function` - Custom `delete_many` function override
851///
852/// # Field-Level Attributes
853///
854/// **Boolean Flags** (can be used as just `flag` or `flag = true/false`):
855/// - `primary_key` - Mark field as primary key (only one allowed)
856/// - `sortable` - Include field in `sortable_columns()`
857/// - `filterable` - Include field in `filterable_columns()`
858/// - `fulltext` - Enable full-text search for this field
859/// - `non_db_attr` - Field is not in database, won't appear in DB operations
860/// - `use_target_models` - Use target's Create/Update models instead of full entity model
861///
862/// **Named Parameters**:
863/// - `create_model = false` - Exclude from Create model (default: true)
864/// - `update_model = false` - Exclude from Update model (default: true)
865/// - `list_model = false` - Exclude from List model (default: true)
866/// - `on_create = expression` - Auto-generate value on create (e.g., `Uuid::new_v4()`)
867/// - `on_update = expression` - Auto-generate value on update (e.g., `Utc::now()`)
868/// - `default = expression` - Default value for non-DB fields
869/// - `fulltext_language = "english"` - Language for full-text search
870///
871/// **Model Exclusion** (Rust-idiomatic alternative to negative boolean flags):
872/// - `exclude(create)` - Exclude from Create model (same as `create_model = false`)
873/// - `exclude(update)` - Exclude from Update model (same as `update_model = false`)
874/// - `exclude(list)` - Exclude from List model (same as `list_model = false`)
875/// - `exclude(create, update)` - Exclude from multiple models
876/// - `exclude(create, update, list)` - Exclude from all models
877///
878/// **Join Configuration** (for relationship loading):
879/// - `join(one)` - Load this relationship in `get_one()` calls
880/// - `join(all)` - Load this relationship in `get_all()` calls
881/// - `join(one, all)` - Load in both `get_one()` and `get_all()` calls
882/// - `join(one, all, depth = 2)` - Recursive loading with specified depth
883/// - `join(one, all, relation = "CustomRelation")` - Use custom Sea-ORM relation name
884///
885/// # Example
886///
887/// ```rust,ignore
888/// use uuid::Uuid;
889/// use crudcrate_derive::EntityToModels;
890/// use sea_orm::prelude::*;
891///
892/// #[derive(Clone, Debug, PartialEq, DeriveEntityModel, EntityToModels)]
893/// #[sea_orm(table_name = "customers")]
894/// #[crudcrate(api_struct = "EntityName", generate_router)]
895/// pub struct Model {
896///     #[sea_orm(primary_key, auto_increment = false)]
897///     #[crudcrate(primary_key, exclude(create, update), on_create = Uuid::new_v4())]
898///     pub id: Uuid,
899///
900///     #[crudcrate(sortable, filterable)]
901///     pub name: String,
902///
903///     #[crudcrate(filterable)]
904///     pub email: String,
905///
906///     #[crudcrate(sortable, exclude(create, update), on_create = Utc::now())]
907///     pub created_at: DateTime<Utc>,
908///
909///     #[crudcrate(sortable, exclude(create, update), on_create = Utc::now(), on_update = Utc::now())]
910///     pub updated_at: DateTime<Utc>,
911///
912///     // Join field - loads vehicles automatically with depth=3 recursive loading
913///     #[sea_orm(ignore)]
914///     #[crudcrate(non_db_attr, join(one, all))]  // depth=3 by default
915///     pub related_entities: Vec<RelatedEntity>,
916/// }
917///
918/// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
919/// pub enum Relation {}
920///
921/// impl ActiveModelBehavior for ActiveModel {}
922/// ```
923///
924/// Parse and validate attributes for `EntityToModels` macro
925fn parse_and_validate_entity_attributes(
926    input: &DeriveInput,
927    struct_name: &syn::Ident,
928) -> Result<(String, syn::Ident, String, CRUDResourceMeta), TokenStream> {
929    let (api_struct_name, active_model_path) = parse_entity_attributes(input, struct_name);
930    let table_name = attribute_parser::extract_table_name(&input.attrs)
931        .unwrap_or_else(|| struct_name.to_string());
932
933    let meta = attribute_parser::parse_crud_resource_meta(&input.attrs);
934    let crud_meta = meta.with_defaults(&table_name);
935
936    // Validate active model path
937    if syn::parse_str::<syn::Type>(&active_model_path).is_err() {
938        return Err(syn::Error::new_spanned(
939            input,
940            format!("Invalid active_model path: {active_model_path}"),
941        )
942        .to_compile_error()
943        .into());
944    }
945
946    Ok((table_name, api_struct_name, active_model_path, crud_meta))
947}
948
949/// Setup join validation and entity registration
950fn setup_join_validation(
951    field_analysis: &EntityFieldAnalysis,
952    api_struct_name: &syn::Ident,
953) -> Result<proc_macro2::TokenStream, TokenStream> {
954    // No global registry needed - types are used as explicitly written
955
956    // Generate compile-time validation for join relationships
957    let _join_validation = relation_validator::generate_join_relation_validation(field_analysis);
958
959    // Check for cyclic dependencies and emit compile-time error if detected
960    let cyclic_dependency_check = relation_validator::generate_cyclic_dependency_check(
961        field_analysis,
962        &api_struct_name.to_string(),
963    );
964    if !cyclic_dependency_check.is_empty() {
965        return Err(cyclic_dependency_check.into());
966    }
967
968    Ok(quote! {})
969}
970
971/// Generate core API model components
972fn generate_core_api_models(
973    struct_name: &syn::Ident,
974    api_struct_name: &syn::Ident,
975    crud_meta: &CRUDResourceMeta,
976    active_model_path: &str,
977    field_analysis: &EntityFieldAnalysis,
978    table_name: &str,
979) -> (
980    proc_macro2::TokenStream,
981    proc_macro2::TokenStream,
982    proc_macro2::TokenStream,
983) {
984    let (api_struct_fields, from_model_assignments, required_imports) =
985        generate_api_struct_content(field_analysis, api_struct_name);
986    let api_struct = generate_api_struct(
987        api_struct_name,
988        &api_struct_fields,
989        active_model_path,
990        crud_meta,
991        field_analysis,
992        &required_imports,
993    );
994    let from_impl = generate_from_impl(struct_name, api_struct_name, &from_model_assignments);
995    let crud_impl = generate_conditional_crud_impl(
996        api_struct_name,
997        crud_meta,
998        active_model_path,
999        field_analysis,
1000        table_name,
1001    );
1002
1003    (api_struct, from_impl, crud_impl)
1004}
1005
1006/// Generate list and response models
1007fn generate_list_and_response_models(
1008    input: &DeriveInput,
1009    api_struct_name: &syn::Ident,
1010    struct_name: &syn::Ident,
1011    field_analysis: &EntityFieldAnalysis,
1012) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
1013    // Generate List model
1014    let list_name = format_ident!("{}List", api_struct_name);
1015    let raw_fields = extract_named_fields(input);
1016    let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&raw_fields);
1017    let list_from_assignments =
1018        crate::codegen::models::list::generate_list_from_assignments(&raw_fields);
1019    let list_from_model_assignments =
1020        crate::codegen::models::list::generate_list_from_model_assignments(field_analysis);
1021
1022    let list_derives =
1023        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1024
1025    let list_model = quote! {
1026        #[derive(#list_derives)]
1027        pub struct #list_name {
1028            #(#list_struct_fields),*
1029        }
1030
1031        impl From<#api_struct_name> for #list_name {
1032            fn from(model: #api_struct_name) -> Self {
1033                Self {
1034                    #(#list_from_assignments),*
1035                }
1036            }
1037        }
1038
1039        impl From<#struct_name> for #list_name {
1040            fn from(model: #struct_name) -> Self {
1041                Self {
1042                    #(#list_from_model_assignments),*
1043                }
1044            }
1045        }
1046    };
1047
1048    // Generate Response model
1049    let response_name = format_ident!("{}Response", api_struct_name);
1050    let response_struct_fields =
1051        crate::codegen::models::response::generate_response_struct_fields(&raw_fields);
1052    let response_from_assignments =
1053        crate::codegen::models::response::generate_response_from_assignments(&raw_fields);
1054
1055    let response_derives =
1056        quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
1057
1058    let response_model = quote! {
1059        #[derive(#response_derives)]
1060        pub struct #response_name {
1061            #(#response_struct_fields),*
1062        }
1063
1064        impl From<#api_struct_name> for #response_name {
1065            fn from(model: #api_struct_name) -> Self {
1066                Self {
1067                    #(#response_from_assignments),*
1068                }
1069            }
1070        }
1071    };
1072
1073    (list_model, response_model)
1074}
1075
1076/// # Panics
1077///
1078/// This function will panic in the following cases:
1079/// - When deprecated syntax is used (e.g., `create_model = false` instead of `exclude(create)`)
1080/// - When there are cyclic join dependencies without explicit depth specification
1081/// - When required Sea-ORM relation enums are missing for join fields
1082#[proc_macro_derive(EntityToModels, attributes(crudcrate))]
1083pub fn entity_to_models(input: TokenStream) -> TokenStream {
1084    let input = parse_macro_input!(input as DeriveInput);
1085    let struct_name = &input.ident;
1086
1087    // Parse and validate attributes
1088    let (table_name, api_struct_name, active_model_path, crud_meta) =
1089        match parse_and_validate_entity_attributes(&input, struct_name) {
1090            Ok(result) => result,
1091            Err(e) => return e,
1092        };
1093
1094    // Extract fields and create field analysis
1095    let fields = match extract_entity_fields(&input) {
1096        Ok(f) => f,
1097        Err(e) => return e,
1098    };
1099    let field_analysis = analyze_entity_fields(fields);
1100    if let Err(e) = validate_field_analysis(&field_analysis) {
1101        return e;
1102    }
1103
1104    // Setup join validation and entity registration
1105    let _join_validation = match setup_join_validation(&field_analysis, &api_struct_name) {
1106        Ok(validation) => validation,
1107        Err(e) => return e,
1108    };
1109
1110    // Generate core API model components
1111    let (api_struct, from_impl, crud_impl) = generate_core_api_models(
1112        struct_name,
1113        &api_struct_name,
1114        &crud_meta,
1115        &active_model_path,
1116        &field_analysis,
1117        &table_name,
1118    );
1119
1120    // Generate list and response models
1121    let (list_model, response_model) =
1122        generate_list_and_response_models(&input, &api_struct_name, struct_name, &field_analysis);
1123
1124    // Generate final output
1125    let expanded = quote! {
1126        #api_struct
1127        #from_impl
1128        #crud_impl
1129        #list_model
1130        #response_model
1131        #_join_validation
1132    };
1133
1134    TokenStream::from(expanded)
1135}