vespera_macro 0.1.43

Procedural macros for Vespera - automatic route discovery and OpenAPI generation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
//! Schema macro implementation
//!
//! Provides macros for generating `OpenAPI` schemas from struct types:
//! - `schema!` - Generate Schema value with optional field filtering
//! - `schema_type!` - Generate new struct type derived from existing type

mod circular;
mod codegen;
pub mod file_cache;
mod file_lookup;
mod from_model;
mod inline_types;
mod input;
mod seaorm;
mod transformation;
pub mod type_utils;
mod validation;

pub use file_cache::print_profile_summary;

use std::collections::{HashMap, HashSet};

use codegen::generate_filtered_schema;
use file_lookup::find_struct_from_path;
use from_model::generate_from_model_with_relations;
use inline_types::{
    generate_inline_relation_type, generate_inline_relation_type_no_relations,
    generate_inline_type_definition,
};
pub use input::{PartialMode, SchemaInput, SchemaTypeInput};
use proc_macro2::TokenStream;
use quote::quote;
use seaorm::{
    RelationFieldInfo, convert_relation_type_to_schema_with_info, convert_type_with_chrono,
    extract_sea_orm_default_value, has_sea_orm_primary_key, is_sql_function_default,
};
use transformation::{
    build_omit_set, build_partial_config, build_pick_set, build_rename_map, determine_rename_all,
    extract_doc_attrs, extract_field_serde_attrs, extract_form_data_attrs,
    extract_serde_attrs_without_rename_all, filter_out_serde_rename, should_skip_field,
    should_wrap_in_option,
};
use type_utils::{
    extract_module_path, extract_type_name, is_option_type, is_qualified_path, is_seaorm_model,
    is_seaorm_relation_type,
};
use validation::{
    extract_source_field_names, validate_omit_fields, validate_partial_fields,
    validate_pick_fields, validate_rename_fields,
};

use crate::{
    metadata::StructMetadata,
    parser::{extract_default, extract_field_rename, strip_raw_prefix_owned},
};

/// Generate schema code from a struct with optional field filtering
pub fn generate_schema_code(
    input: &SchemaInput,
    schema_storage: &HashMap<String, StructMetadata>,
) -> Result<TokenStream, syn::Error> {
    // Extract type name from the Type
    let type_name = extract_type_name(&input.ty)?;

    // Find struct definition in storage (O(1) HashMap lookup)
    let struct_def = schema_storage.get(&type_name).ok_or_else(|| syn::Error::new_spanned(&input.ty, format!("type `{type_name}` not found. Make sure it has #[derive(Schema)] before this macro invocation")))?;

    // Parse the struct definition
    let parsed_struct: syn::ItemStruct = file_cache::parse_struct_cached(&struct_def.definition)
        .map_err(|e| {
            syn::Error::new_spanned(
                &input.ty,
                format!("failed to parse struct definition for `{type_name}`: {e}"),
            )
        })?;

    // Build omit set
    let omit_set: HashSet<String> = input.omit.iter().flatten().cloned().collect();

    // Build pick set
    let pick_set: HashSet<String> = input.pick.iter().flatten().cloned().collect();

    // Generate schema with filtering
    let schema_tokens =
        generate_filtered_schema(&parsed_struct, &omit_set, &pick_set, schema_storage);

    Ok(schema_tokens)
}

/// Generate a new struct type from an existing type with field filtering
///
/// Returns (`TokenStream`, Option<StructMetadata>) where the metadata is returned
/// when a custom `name` is provided (for direct registration in `SCHEMA_STORAGE`).
#[allow(clippy::too_many_lines)]
pub fn generate_schema_type_code(
    input: &SchemaTypeInput,
    schema_storage: &HashMap<String, StructMetadata>,
) -> Result<(TokenStream, Option<StructMetadata>), syn::Error> {
    // Extract type name from the source Type
    let source_type_name = extract_type_name(&input.source_type)?;

    // Extract the module path for resolving relative paths in relation types
    // This may be empty for simple names like `Model` - will be overridden below if found from file
    let mut source_module_path = extract_module_path(&input.source_type);

    // Find struct definition - check SCHEMA_STORAGE first (no file I/O),
    // fall back to file lookup for types not registered (e.g., SeaORM Model).
    let struct_def_owned: StructMetadata;
    let schema_name_hint = input.schema_name.as_deref();
    let struct_def = if is_qualified_path(&input.source_type) {
        // Qualified path: try storage first (avoids parse_file for Schema-derived types),
        // then file lookup for non-Schema types (e.g., SeaORM Model)
        if let Some(found) = schema_storage.get(&source_type_name) {
            found
        } else if let Some((found, module_path)) =
            find_struct_from_path(&input.source_type, schema_name_hint)
        {
            struct_def_owned = found;
            // Use the module path from file lookup for qualified paths
            // The file lookup derives module path from actual file location, which is more accurate
            // for resolving relative paths like `super::user::Entity`
            source_module_path = module_path;
            &struct_def_owned
        } else {
            return Err(syn::Error::new_spanned(
                &input.source_type,
                format!(
                    "type `{source_type_name}` not found. Either:\n\
                     1. Use #[derive(Schema)] in the same file\n\
                     2. Use full module path like `crate::models::memo::Model` to reference a struct from another file"
                ),
            ));
        }
    } else {
        // Simple name: try storage first (for same-file structs), then file lookup with schema name hint
        if let Some(found) = schema_storage.get(&source_type_name) {
            found
        } else if let Some((found, module_path)) =
            find_struct_from_path(&input.source_type, schema_name_hint)
        {
            struct_def_owned = found;
            // For simple names, we MUST use the inferred module path from the file location
            // This is crucial for resolving relative paths like `super::user::Entity`
            source_module_path = module_path;
            &struct_def_owned
        } else {
            return Err(syn::Error::new_spanned(
                &input.source_type,
                format!(
                    "type `{source_type_name}` not found. Either:\n\
                     1. Use #[derive(Schema)] in the same file\n\
                     2. Use full module path like `crate::models::memo::Model` to reference a struct from another file\n\
                     3. If using `name = \"XxxSchema\"`, ensure the file name matches (e.g., xxx.rs)"
                ),
            ));
        }
    };

    // Parse the struct definition
    let parsed_struct: syn::ItemStruct = file_cache::parse_struct_cached(&struct_def.definition)
        .map_err(|e| {
            syn::Error::new_spanned(
                &input.source_type,
                format!("failed to parse struct definition for `{source_type_name}`: {e}"),
            )
        })?;

    // Extract all field names from source struct for validation
    // Include relation fields since they can be converted to Schema types
    let source_field_names = extract_source_field_names(&parsed_struct);

    // Validate all field references exist in source struct
    validate_pick_fields(
        input.pick.as_ref(),
        &source_field_names,
        &input.source_type,
        &source_type_name,
    )?;
    validate_omit_fields(
        input.omit.as_ref(),
        &source_field_names,
        &input.source_type,
        &source_type_name,
    )?;
    validate_rename_fields(
        input.rename.as_ref(),
        &source_field_names,
        &input.source_type,
        &source_type_name,
    )?;
    let partial_fields_to_validate = match &input.partial {
        Some(PartialMode::Fields(fields)) => Some(fields),
        _ => None,
    };
    validate_partial_fields(
        partial_fields_to_validate,
        &source_field_names,
        &input.source_type,
        &source_type_name,
    )?;

    // Build filter sets and rename map
    let omit_set = build_omit_set(input.omit.as_ref());
    let pick_set = build_pick_set(input.pick.as_ref());
    let (partial_all, partial_set) = build_partial_config(&input.partial);
    let rename_map = build_rename_map(input.rename.as_ref());

    // Extract serde attributes from source struct, excluding rename_all (we'll handle it separately)
    let serde_attrs_without_rename_all =
        extract_serde_attrs_without_rename_all(&parsed_struct.attrs);

    // Extract doc comments from source struct to carry over to generated struct
    let struct_doc_attrs = extract_doc_attrs(&parsed_struct.attrs);

    // Determine the effective rename_all strategy
    let effective_rename_all =
        determine_rename_all(input.rename_all.as_ref(), &parsed_struct.attrs);

    // Check if source is a SeaORM Model
    let is_source_seaorm_model = is_seaorm_model(&parsed_struct);

    // Generate new struct with filtered fields
    let new_type_name = &input.new_type;
    let mut field_tokens = Vec::new();
    // Track field mappings for From impl: (new_field_ident, source_field_ident, wrapped_in_option, is_relation)
    let mut field_mappings: Vec<(syn::Ident, syn::Ident, bool, bool)> = Vec::new();
    // Track relation field info for from_model generation
    let mut relation_fields: Vec<RelationFieldInfo> = Vec::new();
    // Track inline types that need to be generated for circular relations
    let mut inline_type_definitions: Vec<TokenStream> = Vec::new();
    // Track default value functions generated from sea_orm(default_value)
    let mut default_functions: Vec<TokenStream> = Vec::new();

    if let syn::Fields::Named(fields_named) = &parsed_struct.fields {
        for field in &fields_named.named {
            let rust_field_name = field.ident.as_ref().map_or_else(
                || "unknown".to_string(),
                |i| strip_raw_prefix_owned(i.to_string()),
            );

            // Apply omit/pick filters
            if should_skip_field(&rust_field_name, &omit_set, &pick_set) {
                continue;
            }

            // Apply omit_default: skip fields with sea_orm(default_value) or sea_orm(primary_key)
            if input.omit_default
                && (extract_sea_orm_default_value(&field.attrs).is_some()
                    || has_sea_orm_primary_key(&field.attrs))
            {
                continue;
            }

            // Check if this is a SeaORM relation type
            let is_relation = is_seaorm_relation_type(&field.ty);

            // In multipart mode, skip ALL relation fields (multipart forms can't represent nested objects)
            if input.multipart && is_relation {
                continue;
            }

            // Get field components, applying partial wrapping if needed
            let original_ty = &field.ty;
            let should_wrap_option = should_wrap_in_option(
                &rust_field_name,
                partial_all,
                &partial_set,
                is_option_type(original_ty),
                is_relation,
            );

            // Determine field type: convert relation types to Schema types
            let (field_ty, relation_info): (Box<dyn quote::ToTokens>, Option<RelationFieldInfo>) =
                if is_relation {
                    // Convert HasOne/HasMany/BelongsTo to Schema type
                    if let Some((converted, mut rel_info)) =
                        convert_relation_type_to_schema_with_info(
                            original_ty,
                            &field.attrs,
                            &parsed_struct,
                            &source_module_path,
                            field.ident.clone().unwrap(),
                        )
                    {
                        // NEW RULE: HasMany (reverse references) are excluded by default
                        // They can only be included via explicit `pick`
                        if rel_info.relation_type == "HasMany" {
                            // HasMany is only included if explicitly picked
                            if !pick_set.contains(&rust_field_name) {
                                continue;
                            }
                            // When HasMany IS picked, generate inline type with ALL relations stripped
                            if let Some(inline_type) = generate_inline_relation_type_no_relations(
                                new_type_name,
                                &rel_info,
                                &source_module_path,
                                input.schema_name.as_deref(),
                            ) {
                                let inline_type_def = generate_inline_type_definition(&inline_type);
                                inline_type_definitions.push(inline_type_def);

                                let inline_type_name = &inline_type.type_name;
                                let included_fields: Vec<String> = inline_type
                                    .fields
                                    .iter()
                                    .map(|f| f.name.to_string())
                                    .collect();

                                rel_info.inline_type_info =
                                    Some((inline_type.type_name.clone(), included_fields));

                                let inline_field_ty = quote! { Vec<#inline_type_name> };
                                (Box::new(inline_field_ty), Some(rel_info))
                            } else {
                                continue;
                            }
                        } else {
                            // BelongsTo/HasOne: Include by default
                            // Check for circular references and potentially use inline type
                            if let Some(inline_type) = generate_inline_relation_type(
                                new_type_name,
                                &rel_info,
                                &source_module_path,
                                input.schema_name.as_deref(),
                            ) {
                                // Generate inline type definition
                                let inline_type_def = generate_inline_type_definition(&inline_type);
                                inline_type_definitions.push(inline_type_def);

                                // Use inline type instead of direct schema reference
                                let inline_type_name = &inline_type.type_name;
                                let circular_fields: Vec<String> = inline_type
                                    .fields
                                    .iter()
                                    .map(|f| f.name.to_string())
                                    .collect();

                                // Store inline type info
                                rel_info.inline_type_info =
                                    Some((inline_type.type_name.clone(), circular_fields));

                                // Generate field type using inline type
                                let inline_field_ty = if rel_info.is_optional {
                                    quote! { Option<Box<#inline_type_name>> }
                                } else {
                                    quote! { Box<#inline_type_name> }
                                };

                                (Box::new(inline_field_ty), Some(rel_info))
                            } else {
                                // No circular refs, use original schema path
                                (Box::new(converted), Some(rel_info))
                            }
                        }
                    } else {
                        // Fallback: skip if conversion fails
                        continue;
                    }
                } else {
                    // Convert SeaORM datetime types to chrono equivalents
                    // Also resolves local types to absolute paths
                    let converted_ty = convert_type_with_chrono(original_ty, &source_module_path);
                    if should_wrap_option {
                        (Box::new(quote! { Option<#converted_ty> }), None)
                    } else {
                        (Box::new(converted_ty), None)
                    }
                };

            // Collect relation info
            if let Some(info) = relation_info {
                relation_fields.push(info);
            }
            let vis = &field.vis;
            let source_field_ident = field.ident.clone().unwrap();

            // Extract doc attributes to carry over comments to the generated struct
            let doc_attrs = extract_doc_attrs(&field.attrs);

            if input.multipart {
                // Multipart mode: emit form_data attrs, suppress serde attrs
                let form_data_attrs = extract_form_data_attrs(&field.attrs);

                // Check if field should be renamed (rename still applies to Rust field names)
                if let Some(new_name) = rename_map.get(&rust_field_name) {
                    let new_field_ident =
                        syn::Ident::new(new_name, field.ident.as_ref().unwrap().span());

                    field_tokens.push(quote! {
                        #(#doc_attrs)*
                        #(#form_data_attrs)*
                        #vis #new_field_ident: #field_ty
                    });

                    field_mappings.push((
                        new_field_ident,
                        source_field_ident,
                        should_wrap_option,
                        is_relation,
                    ));
                } else {
                    let field_ident = field.ident.clone().unwrap();

                    field_tokens.push(quote! {
                        #(#doc_attrs)*
                        #(#form_data_attrs)*
                        #vis #field_ident: #field_ty
                    });

                    field_mappings.push((
                        field_ident.clone(),
                        field_ident,
                        should_wrap_option,
                        is_relation,
                    ));
                }
            } else {
                // Normal (serde) mode: emit serde attrs
                // Filter field attributes: keep serde and doc attributes, remove sea_orm and others
                // This is important when using schema_type! with models from other files
                // that may have ORM-specific attributes we don't want in the generated struct
                let serde_field_attrs = extract_field_serde_attrs(&field.attrs);

                // Generate serde default + schema(default) from sea_orm(default_value) or primary_key
                // Handles literal defaults, SQL function defaults, and implicit auto-increment
                let (serde_default_attr, schema_default_attr) = generate_sea_orm_default_attrs(
                    &field.attrs,
                    new_type_name,
                    &rust_field_name,
                    original_ty,
                    &field_ty,
                    should_wrap_option || is_option_type(original_ty),
                    &mut default_functions,
                );

                // Check if field should be renamed
                if let Some(new_name) = rename_map.get(&rust_field_name) {
                    // Create new identifier for the field
                    let new_field_ident =
                        syn::Ident::new(new_name, field.ident.as_ref().unwrap().span());

                    // Filter out serde(rename) attributes from the serde attrs
                    let filtered_attrs = filter_out_serde_rename(&serde_field_attrs);

                    // Determine the JSON name: use existing serde(rename) if present, otherwise rust field name
                    let json_name = extract_field_rename(&field.attrs)
                        .unwrap_or_else(|| rust_field_name.clone());

                    field_tokens.push(quote! {
                        #(#doc_attrs)*
                        #(#filtered_attrs)*
                        #serde_default_attr
                        #schema_default_attr
                        #[serde(rename = #json_name)]
                        #vis #new_field_ident: #field_ty
                    });

                    // Track mapping: new field name <- source field name
                    field_mappings.push((
                        new_field_ident,
                        source_field_ident,
                        should_wrap_option,
                        is_relation,
                    ));
                } else {
                    // No rename, keep field with serde and doc attrs
                    let field_ident = field.ident.clone().unwrap();

                    field_tokens.push(quote! {
                        #(#doc_attrs)*
                        #(#serde_field_attrs)*
                        #serde_default_attr
                        #schema_default_attr
                        #vis #field_ident: #field_ty
                    });

                    // Track mapping: same name
                    field_mappings.push((
                        field_ident.clone(),
                        field_ident,
                        should_wrap_option,
                        is_relation,
                    ));
                }
            }
        }
    }

    // Add new fields from `add` parameter
    if let Some(ref add_fields) = input.add {
        for (field_name, field_ty) in add_fields {
            let field_ident = syn::Ident::new(field_name, proc_macro2::Span::call_site());
            field_tokens.push(quote! {
                pub #field_ident: #field_ty
            });
        }
    }

    // Build derive list
    // In multipart mode, force clone = false (FieldData<NamedTempFile> doesn't implement Clone)
    let derive_clone = if input.multipart {
        false
    } else {
        input.derive_clone
    };
    let clone_derive = if derive_clone {
        quote! { Clone, }
    } else {
        quote! {}
    };

    // Conditionally include Schema derive based on ignore_schema flag
    // Also generate #[schema(name = "...")] attribute if custom name is provided AND Schema is derived
    let (schema_derive, schema_name_attr) = if input.ignore_schema {
        (quote! {}, quote! {})
    } else if let Some(ref name) = input.schema_name {
        (
            quote! { vespera::Schema },
            quote! { #[schema(name = #name)] },
        )
    } else {
        (quote! { vespera::Schema }, quote! {})
    };

    // Check if there are any relation fields
    let has_relation_fields = field_mappings.iter().any(|(_, _, _, is_rel)| *is_rel);

    // In multipart mode, skip From and from_model impls entirely
    let source_type = &input.source_type;
    let (from_impl, from_model_impl) = if input.multipart {
        (quote! {}, quote! {})
    } else {
        // Generate From impl only if:
        // 1. `add` is not used (can't auto-populate added fields)
        // 2. There are no relation fields (relation fields don't exist on source Model)
        let from_impl = if input.add.is_none() && !has_relation_fields {
            let field_assignments: Vec<_> = field_mappings
                .iter()
                .map(|(new_ident, source_ident, wrapped, _is_relation)| {
                    if *wrapped {
                        quote! { #new_ident: Some(source.#source_ident) }
                    } else {
                        quote! { #new_ident: source.#source_ident }
                    }
                })
                .collect();

            quote! {
                impl From<#source_type> for #new_type_name {
                    fn from(source: #source_type) -> Self {
                        Self {
                            #(#field_assignments),*
                        }
                    }
                }
            }
        } else {
            quote! {}
        };

        // Generate from_model impl for SeaORM Models WITH relations
        // - No relations: Use `From` trait (generated above)
        // - Has relations: async fn from_model(model: Model, db: &DatabaseConnection) -> Result<Self, DbErr>
        let from_model_impl =
            if is_source_seaorm_model && input.add.is_none() && has_relation_fields {
                generate_from_model_with_relations(
                    new_type_name,
                    source_type,
                    &field_mappings,
                    &relation_fields,
                    &source_module_path,
                    schema_storage,
                )
            } else {
                quote! {}
            };

        (from_impl, from_model_impl)
    };

    // Generate the new struct (with inline types for circular relations first)
    let generated_tokens = if input.multipart {
        // Multipart mode: derive Multipart instead of serde
        // Emit #[serde(rename_all = ...)] so Multipart applies the rename at runtime
        // AND Schema derive reads it via extract_rename_all() fallback for OpenAPI field naming
        quote! {
            #(#inline_type_definitions)*

            #(#struct_doc_attrs)*
            #[derive(vespera::Multipart, #clone_derive #schema_derive)]
            #schema_name_attr
            #[serde(rename_all = #effective_rename_all)]
            pub struct #new_type_name {
                #(#field_tokens),*
            }
        }
    } else {
        // Normal serde mode
        quote! {
            // Inline types for circular relation references
            #(#inline_type_definitions)*

            // Default value functions for sea_orm(default_value) fields
            #(#default_functions)*

            #(#struct_doc_attrs)*
            #[derive(serde::Serialize, serde::Deserialize, #clone_derive #schema_derive)]
            #schema_name_attr
            #[serde(rename_all = #effective_rename_all)]
            #(#serde_attrs_without_rename_all)*
            pub struct #new_type_name {
                #(#field_tokens),*
            }

            #from_impl
            #from_model_impl
        }
    };

    // If custom name is provided, create metadata for direct registration
    // This ensures the schema appears in OpenAPI even when `ignore` is set
    let metadata = input.schema_name.as_ref().map(|custom_name| {
        // Build struct definition string for metadata (without derives/attrs for parsing)
        let struct_def = quote! {
            #[serde(rename_all = #effective_rename_all)]
            #(#serde_attrs_without_rename_all)*
            pub struct #new_type_name {
                #(#field_tokens),*
            }
        };
        StructMetadata::new(custom_name.clone(), struct_def.to_string())
    });

    Ok((generated_tokens, metadata))
}

/// Generate `#[serde(default = "...")]` and `#[schema(default = "...")]` attributes
/// from `#[sea_orm(default_value = ...)]` or `#[sea_orm(primary_key)]` on source fields.
///
/// Returns `(serde_default_attr, schema_default_attr)` as `TokenStream`s.
/// - `serde_default_attr`: `#[serde(default = "default_structname_field")]` for deserialization
/// - `schema_default_attr`: `#[schema(default = "value")]` for OpenAPI default value
///
/// Also generates a companion default function and appends it to `default_functions`.
///
/// Handles three categories of defaults:
/// 1. **Literal defaults** (`default_value = "42"`, `"draft"`, `0.7`):
///    Generates parse-based default function + schema default.
/// 2. **SQL function defaults** (`default_value = "NOW()"`, `"gen_random_uuid()"`):
///    Generates type-specific default function + schema default with type's zero value.
/// 3. **Primary key** (implicit auto-increment):
///    Treated as having an implicit default — generates type-specific default.
///
/// Skips serde default generation when:
/// - The field is wrapped in `Option` (partial mode or already optional)
/// - The field already has `#[serde(default)]`
/// - For literal defaults: the field type doesn't implement `FromStr`
fn generate_sea_orm_default_attrs(
    original_attrs: &[syn::Attribute],
    struct_name: &syn::Ident,
    field_name: &str,
    original_ty: &syn::Type,
    field_ty: &dyn quote::ToTokens,
    is_optional_or_partial: bool,
    default_functions: &mut Vec<TokenStream>,
) -> (TokenStream, TokenStream) {
    // Don't generate defaults for optional/partial fields
    if is_optional_or_partial {
        return (quote! {}, quote! {});
    }

    // Check for sea_orm(default_value) and sea_orm(primary_key)
    let default_value = extract_sea_orm_default_value(original_attrs);
    let has_pk = has_sea_orm_primary_key(original_attrs);

    // No default source found
    if default_value.is_none() && !has_pk {
        return (quote! {}, quote! {});
    }

    let has_existing_serde_default = extract_default(original_attrs).is_some();

    match &default_value {
        // Literal default (e.g., "42", "draft", "0.7")
        Some(value) if !is_sql_function_default(value) => {
            let schema_default_attr = quote! { #[schema(default = #value)] };

            if has_existing_serde_default {
                return (quote! {}, schema_default_attr);
            }

            if !is_parseable_type(original_ty) {
                return (quote! {}, schema_default_attr);
            }

            let fn_name = format!("default_{struct_name}_{field_name}");
            let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());

            default_functions.push(quote! {
                #[allow(non_snake_case)]
                fn #fn_ident() -> #field_ty {
                    #value.parse().unwrap()
                }
            });

            let serde_default_attr = quote! { #[serde(default = #fn_name)] };
            (serde_default_attr, schema_default_attr)
        }
        // SQL function default (NOW(), gen_random_uuid(), etc.) or primary_key auto-increment
        _ => {
            let Some((default_expr, schema_default_str)) =
                sql_function_default_for_type(original_ty)
            else {
                return (quote! {}, quote! {});
            };

            let schema_default_attr = quote! { #[schema(default = #schema_default_str)] };

            if has_existing_serde_default {
                return (quote! {}, schema_default_attr);
            }

            let fn_name = format!("default_{struct_name}_{field_name}");
            let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());

            default_functions.push(quote! {
                #[allow(non_snake_case)]
                fn #fn_ident() -> #field_ty {
                    #default_expr
                }
            });

            let serde_default_attr = quote! { #[serde(default = #fn_name)] };
            (serde_default_attr, schema_default_attr)
        }
    }
}

/// Return a type-appropriate (Rust default expression, OpenAPI default string) pair
/// for fields with SQL function defaults or implicit auto-increment.
///
/// The Rust expression is used in the generated `#[serde(default = "fn")]` function body.
/// The OpenAPI string is used in `#[schema(default = "value")]`.
fn sql_function_default_for_type(original_ty: &syn::Type) -> Option<(TokenStream, String)> {
    let syn::Type::Path(type_path) = original_ty else {
        return None;
    };
    let segment = type_path.path.segments.last()?;
    let type_name = segment.ident.to_string();

    match type_name.as_str() {
        "DateTimeWithTimeZone" | "DateTimeUtc" | "DateTime" => {
            let expr = quote! {
                vespera::chrono::DateTime::<vespera::chrono::Utc>::UNIX_EPOCH.fixed_offset()
            };
            Some((expr, "1970-01-01T00:00:00+00:00".to_string()))
        }
        "NaiveDateTime" => {
            let expr = quote! {
                vespera::chrono::NaiveDateTime::UNIX_EPOCH
            };
            Some((expr, "1970-01-01T00:00:00".to_string()))
        }
        "NaiveDate" => {
            let expr = quote! {
                vespera::chrono::NaiveDate::default()
            };
            Some((expr, "1970-01-01".to_string()))
        }
        "NaiveTime" | "Time" => {
            let expr = quote! {
                vespera::chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()
            };
            Some((expr, "00:00:00".to_string()))
        }
        "Uuid" => Some((
            quote! { Default::default() },
            "00000000-0000-0000-0000-000000000000".to_string(),
        )),
        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
        | "usize" | "f32" | "f64" | "Decimal" => {
            Some((quote! { Default::default() }, "0".to_string()))
        }
        "bool" => Some((quote! { Default::default() }, "false".to_string())),
        "String" => Some((quote! { Default::default() }, String::new())),
        _ => None,
    }
}

/// Check if a type is known to implement `FromStr` and can use `.parse().unwrap()`.
///
/// Returns true for primitive types, String, and Decimal.
/// Returns false for enums and unknown custom types.
fn is_parseable_type(ty: &syn::Type) -> bool {
    let syn::Type::Path(type_path) = ty else {
        return false;
    };
    let Some(segment) = type_path.path.segments.last() else {
        return false;
    };
    type_utils::PRIMITIVE_TYPE_NAMES.contains(&segment.ident.to_string().as_str())
}

#[cfg(test)]
mod tests;