crudcrate_derive/
lib.rs

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