ferro_type_derive/
lib.rs

1//! Derive macros for ferrotype TypeScript type generation
2//!
3//! This crate provides:
4//! - `#[derive(TypeScript)]` for generating TypeScript type definitions from Rust types
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8use quote::quote;
9use syn::{
10    parse_macro_input, Attribute, Data, DeriveInput, Fields, GenericParam, Generics, Ident, Type,
11};
12
13// ============================================================================
14// ATTRIBUTE PARSING
15// ============================================================================
16
17/// Case conversion strategies for rename_all
18#[derive(Debug, Clone, Copy, PartialEq)]
19enum RenameAll {
20    /// camelCase
21    CamelCase,
22    /// PascalCase
23    PascalCase,
24    /// snake_case
25    SnakeCase,
26    /// SCREAMING_SNAKE_CASE
27    ScreamingSnakeCase,
28    /// kebab-case
29    KebabCase,
30    /// SCREAMING-KEBAB-CASE
31    ScreamingKebabCase,
32}
33
34impl RenameAll {
35    fn from_str(s: &str) -> Option<Self> {
36        match s {
37            "camelCase" => Some(RenameAll::CamelCase),
38            "PascalCase" => Some(RenameAll::PascalCase),
39            "snake_case" => Some(RenameAll::SnakeCase),
40            "SCREAMING_SNAKE_CASE" => Some(RenameAll::ScreamingSnakeCase),
41            "kebab-case" => Some(RenameAll::KebabCase),
42            "SCREAMING-KEBAB-CASE" => Some(RenameAll::ScreamingKebabCase),
43            _ => None,
44        }
45    }
46
47    fn apply(&self, name: &str) -> String {
48        match self {
49            RenameAll::CamelCase => to_camel_case(name),
50            RenameAll::PascalCase => to_pascal_case(name),
51            RenameAll::SnakeCase => to_snake_case(name),
52            RenameAll::ScreamingSnakeCase => to_snake_case(name).to_uppercase(),
53            RenameAll::KebabCase => to_snake_case(name).replace('_', "-"),
54            RenameAll::ScreamingKebabCase => to_snake_case(name).replace('_', "-").to_uppercase(),
55        }
56    }
57}
58
59/// Convert to camelCase
60fn to_camel_case(name: &str) -> String {
61    let mut result = String::new();
62    let mut capitalize_next = false;
63
64    for (i, c) in name.chars().enumerate() {
65        if c == '_' {
66            capitalize_next = true;
67        } else if capitalize_next {
68            result.push(c.to_ascii_uppercase());
69            capitalize_next = false;
70        } else if i == 0 {
71            result.push(c.to_ascii_lowercase());
72        } else {
73            result.push(c);
74        }
75    }
76
77    result
78}
79
80/// Convert to PascalCase
81fn to_pascal_case(name: &str) -> String {
82    let mut result = String::new();
83    let mut capitalize_next = true;
84
85    for c in name.chars() {
86        if c == '_' {
87            capitalize_next = true;
88        } else if capitalize_next {
89            result.push(c.to_ascii_uppercase());
90            capitalize_next = false;
91        } else {
92            result.push(c);
93        }
94    }
95
96    result
97}
98
99/// Convert to snake_case (from PascalCase or camelCase)
100fn to_snake_case(name: &str) -> String {
101    let mut result = String::new();
102
103    for (i, c) in name.chars().enumerate() {
104        if c.is_uppercase() && i > 0 {
105            result.push('_');
106        }
107        result.push(c.to_ascii_lowercase());
108    }
109
110    result
111}
112
113/// Container-level attributes (on struct/enum)
114#[derive(Default)]
115struct ContainerAttrs {
116    /// Rename the type itself
117    rename: Option<String>,
118    /// Rename all fields/variants
119    rename_all: Option<RenameAll>,
120    /// Make newtype structs transparent (use inner type directly)
121    transparent: bool,
122    /// Custom tag field name for enums (default: "type")
123    tag: Option<String>,
124    /// Content field name for adjacently tagged enums
125    content: Option<String>,
126    /// Generate untagged union (no discriminant)
127    untagged: bool,
128    /// Template literal pattern for branded ID types (e.g., "vm-${string}")
129    pattern: Option<String>,
130    /// Namespace path for the type (e.g., "VM::Git" or "VM.Git")
131    namespace: Vec<String>,
132}
133
134impl ContainerAttrs {
135    fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
136        let mut result = ContainerAttrs::default();
137
138        for attr in attrs {
139            if !attr.path().is_ident("ts") {
140                continue;
141            }
142
143            attr.parse_nested_meta(|meta| {
144                if meta.path.is_ident("rename") {
145                    let value: syn::LitStr = meta.value()?.parse()?;
146                    result.rename = Some(value.value());
147                } else if meta.path.is_ident("rename_all") {
148                    let value: syn::LitStr = meta.value()?.parse()?;
149                    let s = value.value();
150                    result.rename_all = RenameAll::from_str(&s);
151                    if result.rename_all.is_none() {
152                        return Err(syn::Error::new_spanned(
153                            value,
154                            format!(
155                                "unknown rename_all value: '{}'. Expected one of: \
156                                camelCase, PascalCase, snake_case, \
157                                SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE",
158                                s
159                            ),
160                        ));
161                    }
162                } else if meta.path.is_ident("transparent") {
163                    result.transparent = true;
164                } else if meta.path.is_ident("tag") {
165                    let value: syn::LitStr = meta.value()?.parse()?;
166                    result.tag = Some(value.value());
167                } else if meta.path.is_ident("content") {
168                    let value: syn::LitStr = meta.value()?.parse()?;
169                    result.content = Some(value.value());
170                } else if meta.path.is_ident("untagged") {
171                    result.untagged = true;
172                } else if meta.path.is_ident("pattern") {
173                    let value: syn::LitStr = meta.value()?.parse()?;
174                    result.pattern = Some(value.value());
175                } else if meta.path.is_ident("namespace") {
176                    let value: syn::LitStr = meta.value()?.parse()?;
177                    // Parse namespace path - supports both "::" and "." as separators
178                    let ns_str = value.value();
179                    result.namespace = ns_str
180                        .split(|c| c == ':' || c == '.')
181                        .filter(|s| !s.is_empty())
182                        .map(|s| s.to_string())
183                        .collect();
184                }
185                Ok(())
186            })?;
187        }
188
189        Ok(result)
190    }
191}
192
193/// Field-level attributes
194#[derive(Default)]
195struct FieldAttrs {
196    /// Rename this specific field
197    rename: Option<String>,
198    /// Skip this field in the generated TypeScript
199    skip: bool,
200    /// Flatten this field's type into the parent object
201    flatten: bool,
202    /// Override the TypeScript type with a custom string
203    type_override: Option<String>,
204    /// Mark this field as optional (with ?)
205    default: bool,
206    /// Inline the type definition instead of using a reference
207    inline: bool,
208}
209
210impl FieldAttrs {
211    fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
212        let mut result = FieldAttrs::default();
213
214        for attr in attrs {
215            if !attr.path().is_ident("ts") {
216                continue;
217            }
218
219            attr.parse_nested_meta(|meta| {
220                if meta.path.is_ident("rename") {
221                    let value: syn::LitStr = meta.value()?.parse()?;
222                    result.rename = Some(value.value());
223                } else if meta.path.is_ident("skip") {
224                    result.skip = true;
225                } else if meta.path.is_ident("flatten") {
226                    result.flatten = true;
227                } else if meta.path.is_ident("type") {
228                    let value: syn::LitStr = meta.value()?.parse()?;
229                    result.type_override = Some(value.value());
230                } else if meta.path.is_ident("default") {
231                    result.default = true;
232                } else if meta.path.is_ident("inline") {
233                    result.inline = true;
234                }
235                Ok(())
236            })?;
237        }
238
239        Ok(result)
240    }
241}
242
243/// Get the effective name for a field, applying rename attributes
244fn get_field_name(
245    original: &str,
246    field_attrs: &FieldAttrs,
247    container_attrs: &ContainerAttrs,
248) -> String {
249    // Field-level rename takes precedence
250    if let Some(ref renamed) = field_attrs.rename {
251        return renamed.clone();
252    }
253
254    // Then apply container-level rename_all
255    if let Some(rename_all) = container_attrs.rename_all {
256        return rename_all.apply(original);
257    }
258
259    // Otherwise use original name
260    original.to_string()
261}
262
263/// Parse a template literal pattern into strings and type names.
264///
265/// For example, `"vm-${string}"` becomes:
266/// - strings: ["vm-", ""]
267/// - types: ["string"]
268///
269/// And `"v${number}.${number}.${number}"` becomes:
270/// - strings: ["v", ".", ".", ""]
271/// - types: ["number", "number", "number"]
272fn parse_template_pattern(pattern: &str) -> syn::Result<(Vec<String>, Vec<String>)> {
273    let mut strings = Vec::new();
274    let mut types = Vec::new();
275    let mut current = String::new();
276    let mut chars = pattern.chars().peekable();
277
278    while let Some(c) = chars.next() {
279        if c == '$' && chars.peek() == Some(&'{') {
280            // Found ${...}
281            strings.push(std::mem::take(&mut current));
282            chars.next(); // consume '{'
283
284            let mut type_name = String::new();
285            let mut depth = 1;
286            while let Some(tc) = chars.next() {
287                if tc == '{' {
288                    depth += 1;
289                    type_name.push(tc);
290                } else if tc == '}' {
291                    depth -= 1;
292                    if depth == 0 {
293                        break;
294                    }
295                    type_name.push(tc);
296                } else {
297                    type_name.push(tc);
298                }
299            }
300
301            if type_name.is_empty() {
302                return Err(syn::Error::new(
303                    proc_macro2::Span::call_site(),
304                    "Empty type placeholder ${} in pattern",
305                ));
306            }
307            types.push(type_name);
308        } else {
309            current.push(c);
310        }
311    }
312
313    // Push the remaining string (always one more string than types)
314    strings.push(current);
315
316    Ok((strings, types))
317}
318
319/// Convert a type name string to a TypeDef expression.
320/// Supports: string, number, boolean, bigint, and type references.
321fn type_name_to_typedef(name: &str) -> TokenStream2 {
322    match name.trim() {
323        "string" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::String) },
324        "number" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Number) },
325        "boolean" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Boolean) },
326        "bigint" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::BigInt) },
327        "any" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Any) },
328        "unknown" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Unknown) },
329        // For other types, treat as a reference
330        other => {
331            let type_ref = other.trim();
332            quote! { ferro_type::TypeDef::Ref(#type_ref.to_string()) }
333        }
334    }
335}
336
337/// Generate a TemplateLiteral TypeDef expression from parsed pattern.
338fn generate_template_literal_expr(strings: &[String], types: &[String]) -> TokenStream2 {
339    let string_literals: Vec<_> = strings.iter().map(|s| quote! { #s.to_string() }).collect();
340    let type_exprs: Vec<_> = types.iter().map(|t| {
341        let typedef = type_name_to_typedef(t);
342        quote! { Box::new(#typedef) }
343    }).collect();
344
345    quote! {
346        ferro_type::TypeDef::TemplateLiteral {
347            strings: vec![#(#string_literals),*],
348            types: vec![#(#type_exprs),*],
349        }
350    }
351}
352
353/// Derive macro for generating TypeScript type definitions from Rust types.
354///
355/// # Examples
356///
357/// ## Unit variants
358/// ```ignore
359/// #[derive(TypeScript)]
360/// enum Status {
361///     Pending,
362///     Active,
363///     Completed,
364/// }
365/// // Generates: "Pending" | "Active" | "Completed"
366/// ```
367///
368/// ## Tuple variants
369/// ```ignore
370/// #[derive(TypeScript)]
371/// enum Coordinate {
372///     D2(f64, f64),
373///     D3(f64, f64, f64),
374/// }
375/// // Generates: { type: "D2"; value: [number, number] } | { type: "D3"; value: [number, number, number] }
376/// ```
377///
378/// ## Struct variants
379/// ```ignore
380/// #[derive(TypeScript)]
381/// enum Shape {
382///     Circle { center: Point, radius: f64 },
383///     Rectangle { x: f64, y: f64, width: f64, height: f64 },
384/// }
385/// // Generates: { type: "Circle"; center: Point; radius: number } | { type: "Rectangle"; x: number; y: number; width: number; height: number }
386/// ```
387///
388/// ## Structs
389/// ```ignore
390/// #[derive(TypeScript)]
391/// struct User {
392///     id: String,
393///     name: String,
394///     age: i32,
395/// }
396/// // Generates: { id: string; name: string; age: number }
397/// ```
398#[proc_macro_derive(TypeScript, attributes(ts))]
399pub fn derive_typescript(input: TokenStream) -> TokenStream {
400    let input = parse_macro_input!(input as DeriveInput);
401
402    match expand_derive_typescript(&input) {
403        Ok(tokens) => tokens.into(),
404        Err(err) => err.to_compile_error().into(),
405    }
406}
407
408fn expand_derive_typescript(input: &DeriveInput) -> syn::Result<TokenStream2> {
409    let name = &input.ident;
410    let generics = &input.generics;
411
412    // Parse container-level attributes
413    let container_attrs = ContainerAttrs::from_attrs(&input.attrs)?;
414
415    // Use renamed type name if specified, otherwise use original
416    let type_name = container_attrs
417        .rename
418        .clone()
419        .unwrap_or_else(|| name.to_string());
420
421    match &input.data {
422        Data::Enum(data) => {
423            let typedef = generate_enum_typedef(&data.variants, &container_attrs)?;
424            generate_impl(name, &type_name, &container_attrs.namespace, generics, typedef)
425        }
426        Data::Struct(data) => {
427            // Handle transparent newtypes - they become the inner type directly
428            if container_attrs.transparent {
429                if let syn::Fields::Unnamed(fields) = &data.fields {
430                    if fields.unnamed.len() == 1 {
431                        let inner_type = &fields.unnamed.first().unwrap().ty;
432                        return generate_transparent_impl(name, inner_type, generics);
433                    }
434                }
435                return Err(syn::Error::new_spanned(
436                    input,
437                    "#[ts(transparent)] can only be used on newtype structs (single unnamed field)",
438                ));
439            }
440
441            // Handle template literal patterns - typically for branded ID types
442            if let Some(ref pattern) = container_attrs.pattern {
443                let (strings, types) = parse_template_pattern(pattern)?;
444                let typedef = generate_template_literal_expr(&strings, &types);
445                return generate_impl(name, &type_name, &[], generics, typedef);
446            }
447
448            let typedef = generate_struct_typedef(&data.fields, &container_attrs)?;
449            generate_impl(name, &type_name, &container_attrs.namespace, generics, typedef)
450        }
451        Data::Union(_) => {
452            Err(syn::Error::new_spanned(
453                input,
454                "TypeScript derive is not supported for unions",
455            ))
456        }
457    }
458}
459
460fn generate_enum_typedef(
461    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
462    container_attrs: &ContainerAttrs,
463) -> syn::Result<TokenStream2> {
464    if variants.is_empty() {
465        return Err(syn::Error::new(
466            proc_macro2::Span::call_site(),
467            "Cannot derive TypeScript for empty enum",
468        ));
469    }
470
471    // Check if all variants are unit variants (for string literal union type)
472    let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
473
474    // Handle untagged enums: generate plain union without discriminant
475    if container_attrs.untagged {
476        return generate_untagged_enum(variants, container_attrs);
477    }
478
479    // Get tag field name (default: "type")
480    let tag_name = container_attrs.tag.as_deref().unwrap_or("type");
481
482    // Check if using adjacent tagging (content field specified)
483    let content_name = container_attrs.content.as_deref();
484
485    if all_unit {
486        // Generate string literal union: "Pending" | "Active" | "Completed"
487        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
488        for v in variants.iter() {
489            let variant_attrs = FieldAttrs::from_attrs(&v.attrs)?;
490            let name = get_field_name(&v.ident.to_string(), &variant_attrs, container_attrs);
491            variant_exprs.push(
492                quote! { ferro_type::TypeDef::Literal(ferro_type::Literal::String(#name.to_string())) }
493            );
494        }
495
496        Ok(quote! {
497            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
498        })
499    } else {
500        // Generate discriminated union with tag field
501        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
502
503        for variant in variants.iter() {
504            let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
505            let variant_name_str = get_field_name(
506                &variant.ident.to_string(),
507                &variant_attrs,
508                container_attrs,
509            );
510
511            let expr = match &variant.fields {
512                Fields::Unit => {
513                    // { [tag]: "VariantName" }
514                    quote! {
515                        ferro_type::TypeDef::Object(vec![
516                            ferro_type::Field::new(
517                                #tag_name,
518                                ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
519                            )
520                        ])
521                    }
522                }
523                Fields::Unnamed(fields) => {
524                    if let Some(content) = content_name {
525                        // Adjacent tagging: { [tag]: "Variant", [content]: data }
526                        let content_type = if fields.unnamed.len() == 1 {
527                            let field_type = &fields.unnamed.first().unwrap().ty;
528                            type_to_typedef(field_type)
529                        } else {
530                            let field_exprs: Vec<TokenStream2> = fields
531                                .unnamed
532                                .iter()
533                                .map(|f| type_to_typedef(&f.ty))
534                                .collect();
535                            quote! { ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*]) }
536                        };
537                        quote! {
538                            ferro_type::TypeDef::Object(vec![
539                                ferro_type::Field::new(
540                                    #tag_name,
541                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
542                                ),
543                                ferro_type::Field::new(#content, #content_type)
544                            ])
545                        }
546                    } else if fields.unnamed.len() == 1 {
547                        // Newtype variant (internal tagging): { [tag]: "Text"; value: T }
548                        let field_type = &fields.unnamed.first().unwrap().ty;
549                        let type_expr = type_to_typedef(field_type);
550                        quote! {
551                            ferro_type::TypeDef::Object(vec![
552                                ferro_type::Field::new(
553                                    #tag_name,
554                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
555                                ),
556                                ferro_type::Field::new("value", #type_expr)
557                            ])
558                        }
559                    } else {
560                        // Tuple variant (internal tagging): { [tag]: "D2"; value: [T1, T2] }
561                        let field_exprs: Vec<TokenStream2> = fields
562                            .unnamed
563                            .iter()
564                            .map(|f| type_to_typedef(&f.ty))
565                            .collect();
566                        quote! {
567                            ferro_type::TypeDef::Object(vec![
568                                ferro_type::Field::new(
569                                    #tag_name,
570                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
571                                ),
572                                ferro_type::Field::new(
573                                    "value",
574                                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
575                                )
576                            ])
577                        }
578                    }
579                }
580                Fields::Named(fields) => {
581                    let mut field_exprs: Vec<TokenStream2> = Vec::new();
582                    for f in fields.named.iter() {
583                        let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
584                        if field_attrs.skip {
585                            continue;
586                        }
587                        let original_name = f.ident.as_ref().unwrap().to_string();
588                        let field_name = field_attrs.rename.clone().unwrap_or(original_name);
589                        let field_type = &f.ty;
590                        let type_expr = type_to_typedef(field_type);
591                        field_exprs.push(quote! {
592                            ferro_type::Field::new(#field_name, #type_expr)
593                        });
594                    }
595
596                    if let Some(content) = content_name {
597                        // Adjacent tagging: { [tag]: "Variant", [content]: { fields... } }
598                        quote! {
599                            ferro_type::TypeDef::Object(vec![
600                                ferro_type::Field::new(
601                                    #tag_name,
602                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
603                                ),
604                                ferro_type::Field::new(
605                                    #content,
606                                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
607                                )
608                            ])
609                        }
610                    } else {
611                        // Internal tagging: { [tag]: "Circle"; center: Point; radius: number }
612                        quote! {
613                            ferro_type::TypeDef::Object({
614                                let mut fields = vec![
615                                    ferro_type::Field::new(
616                                        #tag_name,
617                                        ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
618                                    )
619                                ];
620                                fields.extend(vec![#(#field_exprs),*]);
621                                fields
622                            })
623                        }
624                    }
625                }
626            };
627            variant_exprs.push(expr);
628        }
629
630        Ok(quote! {
631            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
632        })
633    }
634}
635
636/// Generate untagged enum: plain union without discriminant fields
637fn generate_untagged_enum(
638    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
639    container_attrs: &ContainerAttrs,
640) -> syn::Result<TokenStream2> {
641    let mut variant_exprs: Vec<TokenStream2> = Vec::new();
642
643    for variant in variants.iter() {
644        let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
645        let variant_name_str = get_field_name(
646            &variant.ident.to_string(),
647            &variant_attrs,
648            container_attrs,
649        );
650
651        let expr = match &variant.fields {
652            Fields::Unit => {
653                // Unit variant in untagged enum becomes string literal
654                quote! {
655                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
656                }
657            }
658            Fields::Unnamed(fields) => {
659                if fields.unnamed.len() == 1 {
660                    // Newtype: just the inner type
661                    let field_type = &fields.unnamed.first().unwrap().ty;
662                    type_to_typedef(field_type)
663                } else {
664                    // Tuple: [T1, T2, ...]
665                    let field_exprs: Vec<TokenStream2> = fields
666                        .unnamed
667                        .iter()
668                        .map(|f| type_to_typedef(&f.ty))
669                        .collect();
670                    quote! {
671                        ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
672                    }
673                }
674            }
675            Fields::Named(fields) => {
676                // Struct variant: { field1: T1, field2: T2 }
677                let mut field_exprs: Vec<TokenStream2> = Vec::new();
678                for f in fields.named.iter() {
679                    let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
680                    if field_attrs.skip {
681                        continue;
682                    }
683                    let original_name = f.ident.as_ref().unwrap().to_string();
684                    let field_name = field_attrs.rename.clone().unwrap_or(original_name);
685                    let field_type = &f.ty;
686                    let type_expr = type_to_typedef(field_type);
687                    field_exprs.push(quote! {
688                        ferro_type::Field::new(#field_name, #type_expr)
689                    });
690                }
691                quote! {
692                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
693                }
694            }
695        };
696        variant_exprs.push(expr);
697    }
698
699    Ok(quote! {
700        ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
701    })
702}
703
704fn generate_struct_typedef(
705    fields: &syn::Fields,
706    container_attrs: &ContainerAttrs,
707) -> syn::Result<TokenStream2> {
708    match fields {
709        syn::Fields::Named(fields) => {
710            // Named struct: Object with fields
711            if fields.named.is_empty() {
712                // Empty struct becomes empty object
713                return Ok(quote! { ferro_type::TypeDef::Object(vec![]) });
714            }
715
716            // Separate regular fields from flattened fields
717            let mut regular_field_exprs: Vec<TokenStream2> = Vec::new();
718            let mut flatten_exprs: Vec<TokenStream2> = Vec::new();
719
720            for f in fields.named.iter() {
721                let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
722                // Skip fields marked with #[ts(skip)]
723                if field_attrs.skip {
724                    continue;
725                }
726
727                let field_type = &f.ty;
728
729                if field_attrs.flatten {
730                    // For flattened fields, we extract the inner type's fields at runtime
731                    flatten_exprs.push(quote! {
732                        {
733                            let inner_td = <#field_type as ferro_type::TypeScript>::typescript();
734                            ferro_type::extract_object_fields(&inner_td)
735                        }
736                    });
737                } else {
738                    let original_name = f.ident.as_ref().unwrap().to_string();
739                    let field_name = get_field_name(&original_name, &field_attrs, container_attrs);
740
741                    // Determine the type expression
742                    let type_expr = if let Some(ref type_override) = field_attrs.type_override {
743                        quote! { ferro_type::TypeDef::Ref(#type_override.to_string()) }
744                    } else {
745                        let base_expr = type_to_typedef(field_type);
746                        if field_attrs.inline {
747                            quote! { ferro_type::inline_typedef(#base_expr) }
748                        } else {
749                            base_expr
750                        }
751                    };
752
753                    // Create field (optional if default is set)
754                    if field_attrs.default {
755                        regular_field_exprs.push(quote! {
756                            ferro_type::Field::optional(#field_name, #type_expr)
757                        });
758                    } else {
759                        regular_field_exprs.push(quote! {
760                            ferro_type::Field::new(#field_name, #type_expr)
761                        });
762                    }
763                }
764            }
765
766            // If there are flattened fields, we need to build the vec dynamically
767            if flatten_exprs.is_empty() {
768                Ok(quote! {
769                    ferro_type::TypeDef::Object(vec![#(#regular_field_exprs),*])
770                })
771            } else {
772                Ok(quote! {
773                    {
774                        let mut fields = vec![#(#regular_field_exprs),*];
775                        #(fields.extend(#flatten_exprs);)*
776                        ferro_type::TypeDef::Object(fields)
777                    }
778                })
779            }
780        }
781        syn::Fields::Unnamed(fields) => {
782            // Tuple struct
783            if fields.unnamed.len() == 1 {
784                // Newtype: unwrap to inner type
785                let field_type = &fields.unnamed.first().unwrap().ty;
786                let type_expr = type_to_typedef(field_type);
787                Ok(quote! { #type_expr })
788            } else {
789                // Tuple: [type1, type2, ...]
790                let field_exprs: Vec<TokenStream2> = fields
791                    .unnamed
792                    .iter()
793                    .map(|f| type_to_typedef(&f.ty))
794                    .collect();
795
796                Ok(quote! {
797                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
798                })
799            }
800        }
801        syn::Fields::Unit => {
802            // Unit struct becomes null
803            Ok(quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Null) })
804        }
805    }
806}
807
808/// Convert a Rust type to its TypeScript TypeDef representation.
809/// Uses TypeScript trait for types that implement it.
810fn type_to_typedef(ty: &Type) -> TokenStream2 {
811    quote! { <#ty as ferro_type::TypeScript>::typescript() }
812}
813
814/// Generate implementation for a transparent newtype wrapper.
815/// The TypeScript representation is just the inner type, not wrapped in Named.
816fn generate_transparent_impl(
817    name: &Ident,
818    inner_type: &Type,
819    generics: &Generics,
820) -> syn::Result<TokenStream2> {
821    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
822
823    // Add TypeScript bounds to generic parameters
824    let where_clause = if generics.params.is_empty() {
825        where_clause.cloned()
826    } else {
827        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
828            if let GenericParam::Type(tp) = p {
829                Some(&tp.ident)
830            } else {
831                None
832            }
833        }).collect();
834
835        if type_params.is_empty() {
836            where_clause.cloned()
837        } else {
838            let bounds = type_params.iter().map(|p| {
839                quote! { #p: ferro_type::TypeScript }
840            });
841
842            if let Some(existing_where) = where_clause {
843                let existing_predicates = &existing_where.predicates;
844                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
845            } else {
846                Some(syn::parse_quote! { where #(#bounds),* })
847            }
848        }
849    };
850
851    Ok(quote! {
852        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
853            fn typescript() -> ferro_type::TypeDef {
854                <#inner_type as ferro_type::TypeScript>::typescript()
855            }
856        }
857    })
858}
859
860fn generate_impl(
861    name: &Ident,
862    name_str: &str,
863    namespace: &[String],
864    generics: &Generics,
865    typedef_expr: TokenStream2,
866) -> syn::Result<TokenStream2> {
867    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
868
869    // Add TypeScript bounds to generic parameters
870    let where_clause = if generics.params.is_empty() {
871        where_clause.cloned()
872    } else {
873        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
874            if let GenericParam::Type(tp) = p {
875                Some(&tp.ident)
876            } else {
877                None
878            }
879        }).collect();
880
881        if type_params.is_empty() {
882            where_clause.cloned()
883        } else {
884            let bounds = type_params.iter().map(|p| {
885                quote! { #p: ferro_type::TypeScript }
886            });
887
888            if let Some(existing_where) = where_clause {
889                let existing_predicates = &existing_where.predicates;
890                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
891            } else {
892                Some(syn::parse_quote! { where #(#bounds),* })
893            }
894        }
895    };
896
897    // Generate auto-registration code only for non-generic types
898    // Generic types can't be auto-registered because we need concrete type parameters
899    let registration = if generics.params.is_empty() {
900        let register_name = syn::Ident::new(
901            &format!("__FERRO_TYPE_REGISTER_{}", name.to_string().to_uppercase()),
902            name.span(),
903        );
904        quote! {
905            #[ferro_type::linkme::distributed_slice(ferro_type::TYPESCRIPT_TYPES)]
906            #[linkme(crate = ferro_type::linkme)]
907            static #register_name: fn() -> ferro_type::TypeDef = || <#name as ferro_type::TypeScript>::typescript();
908        }
909    } else {
910        quote! {}
911    };
912
913    // Generate namespace vec
914    let namespace_expr = if namespace.is_empty() {
915        quote! { vec![] }
916    } else {
917        let ns_strings = namespace.iter().map(|s| quote! { #s.to_string() });
918        quote! { vec![#(#ns_strings),*] }
919    };
920
921    Ok(quote! {
922        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
923            fn typescript() -> ferro_type::TypeDef {
924                ferro_type::TypeDef::Named {
925                    namespace: #namespace_expr,
926                    name: #name_str.to_string(),
927                    def: Box::new(#typedef_expr),
928                    module: Some(module_path!().to_string()),
929                }
930            }
931        }
932
933        #registration
934    })
935}