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