ferro_type_derive/
lib.rs

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