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    /// Base type for indexed access (e.g., "Profile" in Profile["login"])
209    index: Option<String>,
210    /// Key for indexed access (e.g., "login" in Profile["login"])
211    key: Option<String>,
212    /// Template literal pattern for this field (e.g., "${TOPIC}::${ULID}")
213    pattern: Option<String>,
214}
215
216impl FieldAttrs {
217    fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
218        let mut result = FieldAttrs::default();
219
220        for attr in attrs {
221            if !attr.path().is_ident("ts") {
222                continue;
223            }
224
225            attr.parse_nested_meta(|meta| {
226                if meta.path.is_ident("rename") {
227                    let value: syn::LitStr = meta.value()?.parse()?;
228                    result.rename = Some(value.value());
229                } else if meta.path.is_ident("skip") {
230                    result.skip = true;
231                } else if meta.path.is_ident("flatten") {
232                    result.flatten = true;
233                } else if meta.path.is_ident("type") {
234                    let value: syn::LitStr = meta.value()?.parse()?;
235                    result.type_override = Some(value.value());
236                } else if meta.path.is_ident("default") {
237                    result.default = true;
238                } else if meta.path.is_ident("inline") {
239                    result.inline = true;
240                } else if meta.path.is_ident("index") {
241                    let value: syn::LitStr = meta.value()?.parse()?;
242                    result.index = Some(value.value());
243                } else if meta.path.is_ident("key") {
244                    let value: syn::LitStr = meta.value()?.parse()?;
245                    result.key = Some(value.value());
246                } else if meta.path.is_ident("pattern") {
247                    let value: syn::LitStr = meta.value()?.parse()?;
248                    result.pattern = Some(value.value());
249                }
250                Ok(())
251            })?;
252        }
253
254        Ok(result)
255    }
256
257    /// Returns true if this field uses indexed access type
258    fn has_indexed_access(&self) -> bool {
259        self.index.is_some() && self.key.is_some()
260    }
261
262    /// Returns true if this field uses a template literal pattern
263    fn has_pattern(&self) -> bool {
264        self.pattern.is_some()
265    }
266}
267
268/// Get the effective name for a field, applying rename attributes
269fn get_field_name(
270    original: &str,
271    field_attrs: &FieldAttrs,
272    container_attrs: &ContainerAttrs,
273) -> String {
274    // Field-level rename takes precedence
275    if let Some(ref renamed) = field_attrs.rename {
276        return renamed.clone();
277    }
278
279    // Then apply container-level rename_all
280    if let Some(rename_all) = container_attrs.rename_all {
281        return rename_all.apply(original);
282    }
283
284    // Otherwise use original name
285    original.to_string()
286}
287
288/// Parse a template literal pattern into strings and type names.
289///
290/// For example, `"vm-${string}"` becomes:
291/// - strings: ["vm-", ""]
292/// - types: ["string"]
293///
294/// And `"v${number}.${number}.${number}"` becomes:
295/// - strings: ["v", ".", ".", ""]
296/// - types: ["number", "number", "number"]
297fn parse_template_pattern(pattern: &str) -> syn::Result<(Vec<String>, Vec<String>)> {
298    let mut strings = Vec::new();
299    let mut types = Vec::new();
300    let mut current = String::new();
301    let mut chars = pattern.chars().peekable();
302
303    while let Some(c) = chars.next() {
304        if c == '$' && chars.peek() == Some(&'{') {
305            // Found ${...}
306            strings.push(std::mem::take(&mut current));
307            chars.next(); // consume '{'
308
309            let mut type_name = String::new();
310            let mut depth = 1;
311            while let Some(tc) = chars.next() {
312                if tc == '{' {
313                    depth += 1;
314                    type_name.push(tc);
315                } else if tc == '}' {
316                    depth -= 1;
317                    if depth == 0 {
318                        break;
319                    }
320                    type_name.push(tc);
321                } else {
322                    type_name.push(tc);
323                }
324            }
325
326            if type_name.is_empty() {
327                return Err(syn::Error::new(
328                    proc_macro2::Span::call_site(),
329                    "Empty type placeholder ${} in pattern",
330                ));
331            }
332            types.push(type_name);
333        } else {
334            current.push(c);
335        }
336    }
337
338    // Push the remaining string (always one more string than types)
339    strings.push(current);
340
341    Ok((strings, types))
342}
343
344/// Convert a type name string to a TypeDef expression.
345/// Supports: string, number, boolean, bigint, and type references.
346fn type_name_to_typedef(name: &str) -> TokenStream2 {
347    match name.trim() {
348        "string" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::String) },
349        "number" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Number) },
350        "boolean" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Boolean) },
351        "bigint" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::BigInt) },
352        "any" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Any) },
353        "unknown" => quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Unknown) },
354        // For other types, treat as a reference
355        other => {
356            let type_ref = other.trim();
357            quote! { ferro_type::TypeDef::Ref(#type_ref.to_string()) }
358        }
359    }
360}
361
362/// Generate a TemplateLiteral TypeDef expression from parsed pattern.
363fn generate_template_literal_expr(strings: &[String], types: &[String]) -> TokenStream2 {
364    let string_literals: Vec<_> = strings.iter().map(|s| quote! { #s.to_string() }).collect();
365    let type_exprs: Vec<_> = types.iter().map(|t| {
366        let typedef = type_name_to_typedef(t);
367        quote! { Box::new(#typedef) }
368    }).collect();
369
370    quote! {
371        ferro_type::TypeDef::TemplateLiteral {
372            strings: vec![#(#string_literals),*],
373            types: vec![#(#type_exprs),*],
374        }
375    }
376}
377
378/// Derive macro for generating TypeScript type definitions from Rust types.
379///
380/// # Examples
381///
382/// ## Unit variants
383/// ```ignore
384/// #[derive(TypeScript)]
385/// enum Status {
386///     Pending,
387///     Active,
388///     Completed,
389/// }
390/// // Generates: "Pending" | "Active" | "Completed"
391/// ```
392///
393/// ## Tuple variants
394/// ```ignore
395/// #[derive(TypeScript)]
396/// enum Coordinate {
397///     D2(f64, f64),
398///     D3(f64, f64, f64),
399/// }
400/// // Generates: { type: "D2"; value: [number, number] } | { type: "D3"; value: [number, number, number] }
401/// ```
402///
403/// ## Struct variants
404/// ```ignore
405/// #[derive(TypeScript)]
406/// enum Shape {
407///     Circle { center: Point, radius: f64 },
408///     Rectangle { x: f64, y: f64, width: f64, height: f64 },
409/// }
410/// // Generates: { type: "Circle"; center: Point; radius: number } | { type: "Rectangle"; x: number; y: number; width: number; height: number }
411/// ```
412///
413/// ## Structs
414/// ```ignore
415/// #[derive(TypeScript)]
416/// struct User {
417///     id: String,
418///     name: String,
419///     age: i32,
420/// }
421/// // Generates: { id: string; name: string; age: number }
422/// ```
423#[proc_macro_derive(TypeScript, attributes(ts))]
424pub fn derive_typescript(input: TokenStream) -> TokenStream {
425    let input = parse_macro_input!(input as DeriveInput);
426
427    match expand_derive_typescript(&input) {
428        Ok(tokens) => tokens.into(),
429        Err(err) => err.to_compile_error().into(),
430    }
431}
432
433fn expand_derive_typescript(input: &DeriveInput) -> syn::Result<TokenStream2> {
434    let name = &input.ident;
435    let generics = &input.generics;
436
437    // Parse container-level attributes
438    let container_attrs = ContainerAttrs::from_attrs(&input.attrs)?;
439
440    // Use renamed type name if specified, otherwise use original
441    let type_name = container_attrs
442        .rename
443        .clone()
444        .unwrap_or_else(|| name.to_string());
445
446    match &input.data {
447        Data::Enum(data) => {
448            let typedef = generate_enum_typedef(&data.variants, &container_attrs)?;
449            generate_impl(name, &type_name, &container_attrs.namespace, generics, typedef)
450        }
451        Data::Struct(data) => {
452            // Handle transparent newtypes - they become the inner type directly
453            if container_attrs.transparent {
454                if let syn::Fields::Unnamed(fields) = &data.fields {
455                    if fields.unnamed.len() == 1 {
456                        let inner_type = &fields.unnamed.first().unwrap().ty;
457                        return generate_transparent_impl(name, inner_type, generics);
458                    }
459                }
460                return Err(syn::Error::new_spanned(
461                    input,
462                    "#[ts(transparent)] can only be used on newtype structs (single unnamed field)",
463                ));
464            }
465
466            // Handle template literal patterns - typically for branded ID types
467            if let Some(ref pattern) = container_attrs.pattern {
468                let (strings, types) = parse_template_pattern(pattern)?;
469                let typedef = generate_template_literal_expr(&strings, &types);
470                return generate_impl(name, &type_name, &[], generics, typedef);
471            }
472
473            let typedef = generate_struct_typedef(&data.fields, &container_attrs)?;
474            generate_impl(name, &type_name, &container_attrs.namespace, generics, typedef)
475        }
476        Data::Union(_) => {
477            Err(syn::Error::new_spanned(
478                input,
479                "TypeScript derive is not supported for unions",
480            ))
481        }
482    }
483}
484
485fn generate_enum_typedef(
486    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
487    container_attrs: &ContainerAttrs,
488) -> syn::Result<TokenStream2> {
489    if variants.is_empty() {
490        return Err(syn::Error::new(
491            proc_macro2::Span::call_site(),
492            "Cannot derive TypeScript for empty enum",
493        ));
494    }
495
496    // Check if all variants are unit variants (for string literal union type)
497    let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
498
499    // Handle untagged enums: generate plain union without discriminant
500    if container_attrs.untagged {
501        return generate_untagged_enum(variants, container_attrs);
502    }
503
504    // Get tag field name (default: "type")
505    let tag_name = container_attrs.tag.as_deref().unwrap_or("type");
506
507    // Check if using adjacent tagging (content field specified)
508    let content_name = container_attrs.content.as_deref();
509
510    if all_unit {
511        // Generate string literal union: "Pending" | "Active" | "Completed"
512        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
513        for v in variants.iter() {
514            let variant_attrs = FieldAttrs::from_attrs(&v.attrs)?;
515            let name = get_field_name(&v.ident.to_string(), &variant_attrs, container_attrs);
516            variant_exprs.push(
517                quote! { ferro_type::TypeDef::Literal(ferro_type::Literal::String(#name.to_string())) }
518            );
519        }
520
521        Ok(quote! {
522            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
523        })
524    } else {
525        // Generate discriminated union with tag field
526        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
527
528        for variant in variants.iter() {
529            let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
530            let variant_name_str = get_field_name(
531                &variant.ident.to_string(),
532                &variant_attrs,
533                container_attrs,
534            );
535
536            let expr = match &variant.fields {
537                Fields::Unit => {
538                    // { [tag]: "VariantName" }
539                    quote! {
540                        ferro_type::TypeDef::Object(vec![
541                            ferro_type::Field::new(
542                                #tag_name,
543                                ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
544                            )
545                        ])
546                    }
547                }
548                Fields::Unnamed(fields) => {
549                    if let Some(content) = content_name {
550                        // Adjacent tagging: { [tag]: "Variant", [content]: data }
551                        let content_type = if fields.unnamed.len() == 1 {
552                            let field_type = &fields.unnamed.first().unwrap().ty;
553                            type_to_typedef(field_type)
554                        } else {
555                            let field_exprs: Vec<TokenStream2> = fields
556                                .unnamed
557                                .iter()
558                                .map(|f| type_to_typedef(&f.ty))
559                                .collect();
560                            quote! { ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*]) }
561                        };
562                        quote! {
563                            ferro_type::TypeDef::Object(vec![
564                                ferro_type::Field::new(
565                                    #tag_name,
566                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
567                                ),
568                                ferro_type::Field::new(#content, #content_type)
569                            ])
570                        }
571                    } else if fields.unnamed.len() == 1 {
572                        // Newtype variant (internal tagging): { [tag]: "Text"; value: T }
573                        let field_type = &fields.unnamed.first().unwrap().ty;
574                        let type_expr = type_to_typedef(field_type);
575                        quote! {
576                            ferro_type::TypeDef::Object(vec![
577                                ferro_type::Field::new(
578                                    #tag_name,
579                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
580                                ),
581                                ferro_type::Field::new("value", #type_expr)
582                            ])
583                        }
584                    } else {
585                        // Tuple variant (internal tagging): { [tag]: "D2"; value: [T1, T2] }
586                        let field_exprs: Vec<TokenStream2> = fields
587                            .unnamed
588                            .iter()
589                            .map(|f| type_to_typedef(&f.ty))
590                            .collect();
591                        quote! {
592                            ferro_type::TypeDef::Object(vec![
593                                ferro_type::Field::new(
594                                    #tag_name,
595                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
596                                ),
597                                ferro_type::Field::new(
598                                    "value",
599                                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
600                                )
601                            ])
602                        }
603                    }
604                }
605                Fields::Named(fields) => {
606                    let mut field_exprs: Vec<TokenStream2> = Vec::new();
607                    for f in fields.named.iter() {
608                        let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
609                        if field_attrs.skip {
610                            continue;
611                        }
612                        let original_name = f.ident.as_ref().unwrap().to_string();
613                        let field_name = field_attrs.rename.clone().unwrap_or(original_name);
614                        let field_type = &f.ty;
615                        let type_expr = type_to_typedef(field_type);
616                        field_exprs.push(quote! {
617                            ferro_type::Field::new(#field_name, #type_expr)
618                        });
619                    }
620
621                    if let Some(content) = content_name {
622                        // Adjacent tagging: { [tag]: "Variant", [content]: { fields... } }
623                        quote! {
624                            ferro_type::TypeDef::Object(vec![
625                                ferro_type::Field::new(
626                                    #tag_name,
627                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
628                                ),
629                                ferro_type::Field::new(
630                                    #content,
631                                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
632                                )
633                            ])
634                        }
635                    } else {
636                        // Internal tagging: { [tag]: "Circle"; center: Point; radius: number }
637                        quote! {
638                            ferro_type::TypeDef::Object({
639                                let mut fields = vec![
640                                    ferro_type::Field::new(
641                                        #tag_name,
642                                        ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
643                                    )
644                                ];
645                                fields.extend(vec![#(#field_exprs),*]);
646                                fields
647                            })
648                        }
649                    }
650                }
651            };
652            variant_exprs.push(expr);
653        }
654
655        Ok(quote! {
656            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
657        })
658    }
659}
660
661/// Generate untagged enum: plain union without discriminant fields
662fn generate_untagged_enum(
663    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
664    container_attrs: &ContainerAttrs,
665) -> syn::Result<TokenStream2> {
666    let mut variant_exprs: Vec<TokenStream2> = Vec::new();
667
668    for variant in variants.iter() {
669        let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
670        let variant_name_str = get_field_name(
671            &variant.ident.to_string(),
672            &variant_attrs,
673            container_attrs,
674        );
675
676        let expr = match &variant.fields {
677            Fields::Unit => {
678                // Unit variant in untagged enum becomes string literal
679                quote! {
680                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
681                }
682            }
683            Fields::Unnamed(fields) => {
684                if fields.unnamed.len() == 1 {
685                    // Newtype: just the inner type
686                    let field_type = &fields.unnamed.first().unwrap().ty;
687                    type_to_typedef(field_type)
688                } else {
689                    // Tuple: [T1, T2, ...]
690                    let field_exprs: Vec<TokenStream2> = fields
691                        .unnamed
692                        .iter()
693                        .map(|f| type_to_typedef(&f.ty))
694                        .collect();
695                    quote! {
696                        ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
697                    }
698                }
699            }
700            Fields::Named(fields) => {
701                // Struct variant: { field1: T1, field2: T2 }
702                let mut field_exprs: Vec<TokenStream2> = Vec::new();
703                for f in fields.named.iter() {
704                    let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
705                    if field_attrs.skip {
706                        continue;
707                    }
708                    let original_name = f.ident.as_ref().unwrap().to_string();
709                    let field_name = field_attrs.rename.clone().unwrap_or(original_name);
710                    let field_type = &f.ty;
711                    let type_expr = type_to_typedef(field_type);
712                    field_exprs.push(quote! {
713                        ferro_type::Field::new(#field_name, #type_expr)
714                    });
715                }
716                quote! {
717                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
718                }
719            }
720        };
721        variant_exprs.push(expr);
722    }
723
724    Ok(quote! {
725        ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
726    })
727}
728
729fn generate_struct_typedef(
730    fields: &syn::Fields,
731    container_attrs: &ContainerAttrs,
732) -> syn::Result<TokenStream2> {
733    match fields {
734        syn::Fields::Named(fields) => {
735            // Named struct: Object with fields
736            if fields.named.is_empty() {
737                // Empty struct becomes empty object
738                return Ok(quote! { ferro_type::TypeDef::Object(vec![]) });
739            }
740
741            // Separate regular fields from flattened fields
742            let mut regular_field_exprs: Vec<TokenStream2> = Vec::new();
743            let mut flatten_exprs: Vec<TokenStream2> = Vec::new();
744
745            for f in fields.named.iter() {
746                let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
747                // Skip fields marked with #[ts(skip)]
748                if field_attrs.skip {
749                    continue;
750                }
751
752                let field_type = &f.ty;
753
754                // Validate indexed access attributes - both must be present or neither
755                if field_attrs.index.is_some() != field_attrs.key.is_some() {
756                    return Err(syn::Error::new_spanned(
757                        f,
758                        "#[ts(index = \"...\")] and #[ts(key = \"...\")] must be used together",
759                    ));
760                }
761
762                if field_attrs.flatten {
763                    // For flattened fields, we extract the inner type's fields at runtime
764                    flatten_exprs.push(quote! {
765                        {
766                            let inner_td = <#field_type as ferro_type::TypeScript>::typescript();
767                            ferro_type::extract_object_fields(&inner_td)
768                        }
769                    });
770                } else {
771                    let original_name = f.ident.as_ref().unwrap().to_string();
772                    let field_name = get_field_name(&original_name, &field_attrs, container_attrs);
773
774                    // Determine the type expression
775                    let type_expr = if let Some(ref type_override) = field_attrs.type_override {
776                        quote! { ferro_type::TypeDef::Ref(#type_override.to_string()) }
777                    } else if field_attrs.has_indexed_access() {
778                        // Use indexed access type: Profile["login"]
779                        let index_base = field_attrs.index.as_ref().unwrap();
780                        let index_key = field_attrs.key.as_ref().unwrap();
781                        quote! {
782                            ferro_type::TypeDef::IndexedAccess {
783                                base: #index_base.to_string(),
784                                key: #index_key.to_string(),
785                            }
786                        }
787                    } else if field_attrs.has_pattern() {
788                        // Use template literal pattern
789                        let pattern = field_attrs.pattern.as_ref().unwrap();
790                        let (strings, types) = parse_template_pattern(pattern)?;
791                        generate_template_literal_expr(&strings, &types)
792                    } else {
793                        let base_expr = type_to_typedef(field_type);
794                        if field_attrs.inline {
795                            quote! { ferro_type::inline_typedef(#base_expr) }
796                        } else {
797                            base_expr
798                        }
799                    };
800
801                    // Create field (optional if default is set)
802                    if field_attrs.default {
803                        regular_field_exprs.push(quote! {
804                            ferro_type::Field::optional(#field_name, #type_expr)
805                        });
806                    } else {
807                        regular_field_exprs.push(quote! {
808                            ferro_type::Field::new(#field_name, #type_expr)
809                        });
810                    }
811                }
812            }
813
814            // If there are flattened fields, we need to build the vec dynamically
815            if flatten_exprs.is_empty() {
816                Ok(quote! {
817                    ferro_type::TypeDef::Object(vec![#(#regular_field_exprs),*])
818                })
819            } else {
820                Ok(quote! {
821                    {
822                        let mut fields = vec![#(#regular_field_exprs),*];
823                        #(fields.extend(#flatten_exprs);)*
824                        ferro_type::TypeDef::Object(fields)
825                    }
826                })
827            }
828        }
829        syn::Fields::Unnamed(fields) => {
830            // Tuple struct
831            if fields.unnamed.len() == 1 {
832                // Newtype: unwrap to inner type
833                let field_type = &fields.unnamed.first().unwrap().ty;
834                let type_expr = type_to_typedef(field_type);
835                Ok(quote! { #type_expr })
836            } else {
837                // Tuple: [type1, type2, ...]
838                let field_exprs: Vec<TokenStream2> = fields
839                    .unnamed
840                    .iter()
841                    .map(|f| type_to_typedef(&f.ty))
842                    .collect();
843
844                Ok(quote! {
845                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
846                })
847            }
848        }
849        syn::Fields::Unit => {
850            // Unit struct becomes null
851            Ok(quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Null) })
852        }
853    }
854}
855
856/// Convert a Rust type to its TypeScript TypeDef representation.
857/// Uses TypeScript trait for types that implement it.
858fn type_to_typedef(ty: &Type) -> TokenStream2 {
859    quote! { <#ty as ferro_type::TypeScript>::typescript() }
860}
861
862/// Generate implementation for a transparent newtype wrapper.
863/// The TypeScript representation is just the inner type, not wrapped in Named.
864fn generate_transparent_impl(
865    name: &Ident,
866    inner_type: &Type,
867    generics: &Generics,
868) -> syn::Result<TokenStream2> {
869    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
870
871    // Add TypeScript bounds to generic parameters
872    let where_clause = if generics.params.is_empty() {
873        where_clause.cloned()
874    } else {
875        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
876            if let GenericParam::Type(tp) = p {
877                Some(&tp.ident)
878            } else {
879                None
880            }
881        }).collect();
882
883        if type_params.is_empty() {
884            where_clause.cloned()
885        } else {
886            let bounds = type_params.iter().map(|p| {
887                quote! { #p: ferro_type::TypeScript }
888            });
889
890            if let Some(existing_where) = where_clause {
891                let existing_predicates = &existing_where.predicates;
892                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
893            } else {
894                Some(syn::parse_quote! { where #(#bounds),* })
895            }
896        }
897    };
898
899    Ok(quote! {
900        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
901            fn typescript() -> ferro_type::TypeDef {
902                <#inner_type as ferro_type::TypeScript>::typescript()
903            }
904        }
905    })
906}
907
908fn generate_impl(
909    name: &Ident,
910    name_str: &str,
911    namespace: &[String],
912    generics: &Generics,
913    typedef_expr: TokenStream2,
914) -> syn::Result<TokenStream2> {
915    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
916
917    // Add TypeScript bounds to generic parameters
918    let where_clause = if generics.params.is_empty() {
919        where_clause.cloned()
920    } else {
921        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
922            if let GenericParam::Type(tp) = p {
923                Some(&tp.ident)
924            } else {
925                None
926            }
927        }).collect();
928
929        if type_params.is_empty() {
930            where_clause.cloned()
931        } else {
932            let bounds = type_params.iter().map(|p| {
933                quote! { #p: ferro_type::TypeScript }
934            });
935
936            if let Some(existing_where) = where_clause {
937                let existing_predicates = &existing_where.predicates;
938                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
939            } else {
940                Some(syn::parse_quote! { where #(#bounds),* })
941            }
942        }
943    };
944
945    // Generate auto-registration code only for non-generic types
946    // Generic types can't be auto-registered because we need concrete type parameters
947    let registration = if generics.params.is_empty() {
948        let register_name = syn::Ident::new(
949            &format!("__FERRO_TYPE_REGISTER_{}", name.to_string().to_uppercase()),
950            name.span(),
951        );
952        quote! {
953            #[ferro_type::linkme::distributed_slice(ferro_type::TYPESCRIPT_TYPES)]
954            #[linkme(crate = ferro_type::linkme)]
955            static #register_name: fn() -> ferro_type::TypeDef = || <#name as ferro_type::TypeScript>::typescript();
956        }
957    } else {
958        quote! {}
959    };
960
961    // Generate namespace vec
962    let namespace_expr = if namespace.is_empty() {
963        quote! { vec![] }
964    } else {
965        let ns_strings = namespace.iter().map(|s| quote! { #s.to_string() });
966        quote! { vec![#(#ns_strings),*] }
967    };
968
969    Ok(quote! {
970        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
971            fn typescript() -> ferro_type::TypeDef {
972                ferro_type::TypeDef::Named {
973                    namespace: #namespace_expr,
974                    name: #name_str.to_string(),
975                    def: Box::new(#typedef_expr),
976                    module: Some(module_path!().to_string()),
977                }
978            }
979        }
980
981        #registration
982    })
983}