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}
129
130impl ContainerAttrs {
131    fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
132        let mut result = ContainerAttrs::default();
133
134        for attr in attrs {
135            if !attr.path().is_ident("ts") {
136                continue;
137            }
138
139            attr.parse_nested_meta(|meta| {
140                if meta.path.is_ident("rename") {
141                    let value: syn::LitStr = meta.value()?.parse()?;
142                    result.rename = Some(value.value());
143                } else if meta.path.is_ident("rename_all") {
144                    let value: syn::LitStr = meta.value()?.parse()?;
145                    let s = value.value();
146                    result.rename_all = RenameAll::from_str(&s);
147                    if result.rename_all.is_none() {
148                        return Err(syn::Error::new_spanned(
149                            value,
150                            format!(
151                                "unknown rename_all value: '{}'. Expected one of: \
152                                camelCase, PascalCase, snake_case, \
153                                SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE",
154                                s
155                            ),
156                        ));
157                    }
158                } else if meta.path.is_ident("transparent") {
159                    result.transparent = true;
160                } else if meta.path.is_ident("tag") {
161                    let value: syn::LitStr = meta.value()?.parse()?;
162                    result.tag = Some(value.value());
163                } else if meta.path.is_ident("content") {
164                    let value: syn::LitStr = meta.value()?.parse()?;
165                    result.content = Some(value.value());
166                } else if meta.path.is_ident("untagged") {
167                    result.untagged = true;
168                }
169                Ok(())
170            })?;
171        }
172
173        Ok(result)
174    }
175}
176
177/// Field-level attributes
178#[derive(Default)]
179struct FieldAttrs {
180    /// Rename this specific field
181    rename: Option<String>,
182    /// Skip this field in the generated TypeScript
183    skip: bool,
184    /// Flatten this field's type into the parent object
185    flatten: bool,
186    /// Override the TypeScript type with a custom string
187    type_override: Option<String>,
188    /// Mark this field as optional (with ?)
189    default: bool,
190    /// Inline the type definition instead of using a reference
191    inline: bool,
192}
193
194impl FieldAttrs {
195    fn from_attrs(attrs: &[Attribute]) -> syn::Result<Self> {
196        let mut result = FieldAttrs::default();
197
198        for attr in attrs {
199            if !attr.path().is_ident("ts") {
200                continue;
201            }
202
203            attr.parse_nested_meta(|meta| {
204                if meta.path.is_ident("rename") {
205                    let value: syn::LitStr = meta.value()?.parse()?;
206                    result.rename = Some(value.value());
207                } else if meta.path.is_ident("skip") {
208                    result.skip = true;
209                } else if meta.path.is_ident("flatten") {
210                    result.flatten = true;
211                } else if meta.path.is_ident("type") {
212                    let value: syn::LitStr = meta.value()?.parse()?;
213                    result.type_override = Some(value.value());
214                } else if meta.path.is_ident("default") {
215                    result.default = true;
216                } else if meta.path.is_ident("inline") {
217                    result.inline = true;
218                }
219                Ok(())
220            })?;
221        }
222
223        Ok(result)
224    }
225}
226
227/// Get the effective name for a field, applying rename attributes
228fn get_field_name(
229    original: &str,
230    field_attrs: &FieldAttrs,
231    container_attrs: &ContainerAttrs,
232) -> String {
233    // Field-level rename takes precedence
234    if let Some(ref renamed) = field_attrs.rename {
235        return renamed.clone();
236    }
237
238    // Then apply container-level rename_all
239    if let Some(rename_all) = container_attrs.rename_all {
240        return rename_all.apply(original);
241    }
242
243    // Otherwise use original name
244    original.to_string()
245}
246
247/// Derive macro for generating TypeScript type definitions from Rust types.
248///
249/// # Examples
250///
251/// ## Unit variants
252/// ```ignore
253/// #[derive(TypeScript)]
254/// enum Status {
255///     Pending,
256///     Active,
257///     Completed,
258/// }
259/// // Generates: "Pending" | "Active" | "Completed"
260/// ```
261///
262/// ## Tuple variants
263/// ```ignore
264/// #[derive(TypeScript)]
265/// enum Coordinate {
266///     D2(f64, f64),
267///     D3(f64, f64, f64),
268/// }
269/// // Generates: { type: "D2"; value: [number, number] } | { type: "D3"; value: [number, number, number] }
270/// ```
271///
272/// ## Struct variants
273/// ```ignore
274/// #[derive(TypeScript)]
275/// enum Shape {
276///     Circle { center: Point, radius: f64 },
277///     Rectangle { x: f64, y: f64, width: f64, height: f64 },
278/// }
279/// // Generates: { type: "Circle"; center: Point; radius: number } | { type: "Rectangle"; x: number; y: number; width: number; height: number }
280/// ```
281///
282/// ## Structs
283/// ```ignore
284/// #[derive(TypeScript)]
285/// struct User {
286///     id: String,
287///     name: String,
288///     age: i32,
289/// }
290/// // Generates: { id: string; name: string; age: number }
291/// ```
292#[proc_macro_derive(TypeScript, attributes(ts))]
293pub fn derive_typescript(input: TokenStream) -> TokenStream {
294    let input = parse_macro_input!(input as DeriveInput);
295
296    match expand_derive_typescript(&input) {
297        Ok(tokens) => tokens.into(),
298        Err(err) => err.to_compile_error().into(),
299    }
300}
301
302fn expand_derive_typescript(input: &DeriveInput) -> syn::Result<TokenStream2> {
303    let name = &input.ident;
304    let generics = &input.generics;
305
306    // Parse container-level attributes
307    let container_attrs = ContainerAttrs::from_attrs(&input.attrs)?;
308
309    // Use renamed type name if specified, otherwise use original
310    let type_name = container_attrs
311        .rename
312        .clone()
313        .unwrap_or_else(|| name.to_string());
314
315    match &input.data {
316        Data::Enum(data) => {
317            let typedef = generate_enum_typedef(&data.variants, &container_attrs)?;
318            generate_impl(name, &type_name, generics, typedef)
319        }
320        Data::Struct(data) => {
321            // Handle transparent newtypes - they become the inner type directly
322            if container_attrs.transparent {
323                if let syn::Fields::Unnamed(fields) = &data.fields {
324                    if fields.unnamed.len() == 1 {
325                        let inner_type = &fields.unnamed.first().unwrap().ty;
326                        return generate_transparent_impl(name, inner_type, generics);
327                    }
328                }
329                return Err(syn::Error::new_spanned(
330                    input,
331                    "#[ts(transparent)] can only be used on newtype structs (single unnamed field)",
332                ));
333            }
334
335            let typedef = generate_struct_typedef(&data.fields, &container_attrs)?;
336            generate_impl(name, &type_name, generics, typedef)
337        }
338        Data::Union(_) => {
339            Err(syn::Error::new_spanned(
340                input,
341                "TypeScript derive is not supported for unions",
342            ))
343        }
344    }
345}
346
347fn generate_enum_typedef(
348    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
349    container_attrs: &ContainerAttrs,
350) -> syn::Result<TokenStream2> {
351    if variants.is_empty() {
352        return Err(syn::Error::new(
353            proc_macro2::Span::call_site(),
354            "Cannot derive TypeScript for empty enum",
355        ));
356    }
357
358    // Check if all variants are unit variants (for string literal union type)
359    let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
360
361    // Handle untagged enums: generate plain union without discriminant
362    if container_attrs.untagged {
363        return generate_untagged_enum(variants, container_attrs);
364    }
365
366    // Get tag field name (default: "type")
367    let tag_name = container_attrs.tag.as_deref().unwrap_or("type");
368
369    // Check if using adjacent tagging (content field specified)
370    let content_name = container_attrs.content.as_deref();
371
372    if all_unit {
373        // Generate string literal union: "Pending" | "Active" | "Completed"
374        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
375        for v in variants.iter() {
376            let variant_attrs = FieldAttrs::from_attrs(&v.attrs)?;
377            let name = get_field_name(&v.ident.to_string(), &variant_attrs, container_attrs);
378            variant_exprs.push(
379                quote! { ferro_type::TypeDef::Literal(ferro_type::Literal::String(#name.to_string())) }
380            );
381        }
382
383        Ok(quote! {
384            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
385        })
386    } else {
387        // Generate discriminated union with tag field
388        let mut variant_exprs: Vec<TokenStream2> = Vec::new();
389
390        for variant in variants.iter() {
391            let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
392            let variant_name_str = get_field_name(
393                &variant.ident.to_string(),
394                &variant_attrs,
395                container_attrs,
396            );
397
398            let expr = match &variant.fields {
399                Fields::Unit => {
400                    // { [tag]: "VariantName" }
401                    quote! {
402                        ferro_type::TypeDef::Object(vec![
403                            ferro_type::Field::new(
404                                #tag_name,
405                                ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
406                            )
407                        ])
408                    }
409                }
410                Fields::Unnamed(fields) => {
411                    if let Some(content) = content_name {
412                        // Adjacent tagging: { [tag]: "Variant", [content]: data }
413                        let content_type = if fields.unnamed.len() == 1 {
414                            let field_type = &fields.unnamed.first().unwrap().ty;
415                            type_to_typedef(field_type)
416                        } else {
417                            let field_exprs: Vec<TokenStream2> = fields
418                                .unnamed
419                                .iter()
420                                .map(|f| type_to_typedef(&f.ty))
421                                .collect();
422                            quote! { ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*]) }
423                        };
424                        quote! {
425                            ferro_type::TypeDef::Object(vec![
426                                ferro_type::Field::new(
427                                    #tag_name,
428                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
429                                ),
430                                ferro_type::Field::new(#content, #content_type)
431                            ])
432                        }
433                    } else if fields.unnamed.len() == 1 {
434                        // Newtype variant (internal tagging): { [tag]: "Text"; value: T }
435                        let field_type = &fields.unnamed.first().unwrap().ty;
436                        let type_expr = type_to_typedef(field_type);
437                        quote! {
438                            ferro_type::TypeDef::Object(vec![
439                                ferro_type::Field::new(
440                                    #tag_name,
441                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
442                                ),
443                                ferro_type::Field::new("value", #type_expr)
444                            ])
445                        }
446                    } else {
447                        // Tuple variant (internal tagging): { [tag]: "D2"; value: [T1, T2] }
448                        let field_exprs: Vec<TokenStream2> = fields
449                            .unnamed
450                            .iter()
451                            .map(|f| type_to_typedef(&f.ty))
452                            .collect();
453                        quote! {
454                            ferro_type::TypeDef::Object(vec![
455                                ferro_type::Field::new(
456                                    #tag_name,
457                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
458                                ),
459                                ferro_type::Field::new(
460                                    "value",
461                                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
462                                )
463                            ])
464                        }
465                    }
466                }
467                Fields::Named(fields) => {
468                    let mut field_exprs: Vec<TokenStream2> = Vec::new();
469                    for f in fields.named.iter() {
470                        let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
471                        if field_attrs.skip {
472                            continue;
473                        }
474                        let original_name = f.ident.as_ref().unwrap().to_string();
475                        let field_name = field_attrs.rename.clone().unwrap_or(original_name);
476                        let field_type = &f.ty;
477                        let type_expr = type_to_typedef(field_type);
478                        field_exprs.push(quote! {
479                            ferro_type::Field::new(#field_name, #type_expr)
480                        });
481                    }
482
483                    if let Some(content) = content_name {
484                        // Adjacent tagging: { [tag]: "Variant", [content]: { fields... } }
485                        quote! {
486                            ferro_type::TypeDef::Object(vec![
487                                ferro_type::Field::new(
488                                    #tag_name,
489                                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
490                                ),
491                                ferro_type::Field::new(
492                                    #content,
493                                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
494                                )
495                            ])
496                        }
497                    } else {
498                        // Internal tagging: { [tag]: "Circle"; center: Point; radius: number }
499                        quote! {
500                            ferro_type::TypeDef::Object({
501                                let mut fields = vec![
502                                    ferro_type::Field::new(
503                                        #tag_name,
504                                        ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
505                                    )
506                                ];
507                                fields.extend(vec![#(#field_exprs),*]);
508                                fields
509                            })
510                        }
511                    }
512                }
513            };
514            variant_exprs.push(expr);
515        }
516
517        Ok(quote! {
518            ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
519        })
520    }
521}
522
523/// Generate untagged enum: plain union without discriminant fields
524fn generate_untagged_enum(
525    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
526    container_attrs: &ContainerAttrs,
527) -> syn::Result<TokenStream2> {
528    let mut variant_exprs: Vec<TokenStream2> = Vec::new();
529
530    for variant in variants.iter() {
531        let variant_attrs = FieldAttrs::from_attrs(&variant.attrs)?;
532        let variant_name_str = get_field_name(
533            &variant.ident.to_string(),
534            &variant_attrs,
535            container_attrs,
536        );
537
538        let expr = match &variant.fields {
539            Fields::Unit => {
540                // Unit variant in untagged enum becomes string literal
541                quote! {
542                    ferro_type::TypeDef::Literal(ferro_type::Literal::String(#variant_name_str.to_string()))
543                }
544            }
545            Fields::Unnamed(fields) => {
546                if fields.unnamed.len() == 1 {
547                    // Newtype: just the inner type
548                    let field_type = &fields.unnamed.first().unwrap().ty;
549                    type_to_typedef(field_type)
550                } else {
551                    // Tuple: [T1, T2, ...]
552                    let field_exprs: Vec<TokenStream2> = fields
553                        .unnamed
554                        .iter()
555                        .map(|f| type_to_typedef(&f.ty))
556                        .collect();
557                    quote! {
558                        ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
559                    }
560                }
561            }
562            Fields::Named(fields) => {
563                // Struct variant: { field1: T1, field2: T2 }
564                let mut field_exprs: Vec<TokenStream2> = Vec::new();
565                for f in fields.named.iter() {
566                    let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
567                    if field_attrs.skip {
568                        continue;
569                    }
570                    let original_name = f.ident.as_ref().unwrap().to_string();
571                    let field_name = field_attrs.rename.clone().unwrap_or(original_name);
572                    let field_type = &f.ty;
573                    let type_expr = type_to_typedef(field_type);
574                    field_exprs.push(quote! {
575                        ferro_type::Field::new(#field_name, #type_expr)
576                    });
577                }
578                quote! {
579                    ferro_type::TypeDef::Object(vec![#(#field_exprs),*])
580                }
581            }
582        };
583        variant_exprs.push(expr);
584    }
585
586    Ok(quote! {
587        ferro_type::TypeDef::Union(vec![#(#variant_exprs),*])
588    })
589}
590
591fn generate_struct_typedef(
592    fields: &syn::Fields,
593    container_attrs: &ContainerAttrs,
594) -> syn::Result<TokenStream2> {
595    match fields {
596        syn::Fields::Named(fields) => {
597            // Named struct: Object with fields
598            if fields.named.is_empty() {
599                // Empty struct becomes empty object
600                return Ok(quote! { ferro_type::TypeDef::Object(vec![]) });
601            }
602
603            // Separate regular fields from flattened fields
604            let mut regular_field_exprs: Vec<TokenStream2> = Vec::new();
605            let mut flatten_exprs: Vec<TokenStream2> = Vec::new();
606
607            for f in fields.named.iter() {
608                let field_attrs = FieldAttrs::from_attrs(&f.attrs)?;
609                // Skip fields marked with #[ts(skip)]
610                if field_attrs.skip {
611                    continue;
612                }
613
614                let field_type = &f.ty;
615
616                if field_attrs.flatten {
617                    // For flattened fields, we extract the inner type's fields at runtime
618                    flatten_exprs.push(quote! {
619                        {
620                            let inner_td = <#field_type as ferro_type::TypeScript>::typescript();
621                            ferro_type::extract_object_fields(&inner_td)
622                        }
623                    });
624                } else {
625                    let original_name = f.ident.as_ref().unwrap().to_string();
626                    let field_name = get_field_name(&original_name, &field_attrs, container_attrs);
627
628                    // Determine the type expression
629                    let type_expr = if let Some(ref type_override) = field_attrs.type_override {
630                        quote! { ferro_type::TypeDef::Ref(#type_override.to_string()) }
631                    } else {
632                        let base_expr = type_to_typedef(field_type);
633                        if field_attrs.inline {
634                            quote! { ferro_type::inline_typedef(#base_expr) }
635                        } else {
636                            base_expr
637                        }
638                    };
639
640                    // Create field (optional if default is set)
641                    if field_attrs.default {
642                        regular_field_exprs.push(quote! {
643                            ferro_type::Field::optional(#field_name, #type_expr)
644                        });
645                    } else {
646                        regular_field_exprs.push(quote! {
647                            ferro_type::Field::new(#field_name, #type_expr)
648                        });
649                    }
650                }
651            }
652
653            // If there are flattened fields, we need to build the vec dynamically
654            if flatten_exprs.is_empty() {
655                Ok(quote! {
656                    ferro_type::TypeDef::Object(vec![#(#regular_field_exprs),*])
657                })
658            } else {
659                Ok(quote! {
660                    {
661                        let mut fields = vec![#(#regular_field_exprs),*];
662                        #(fields.extend(#flatten_exprs);)*
663                        ferro_type::TypeDef::Object(fields)
664                    }
665                })
666            }
667        }
668        syn::Fields::Unnamed(fields) => {
669            // Tuple struct
670            if fields.unnamed.len() == 1 {
671                // Newtype: unwrap to inner type
672                let field_type = &fields.unnamed.first().unwrap().ty;
673                let type_expr = type_to_typedef(field_type);
674                Ok(quote! { #type_expr })
675            } else {
676                // Tuple: [type1, type2, ...]
677                let field_exprs: Vec<TokenStream2> = fields
678                    .unnamed
679                    .iter()
680                    .map(|f| type_to_typedef(&f.ty))
681                    .collect();
682
683                Ok(quote! {
684                    ferro_type::TypeDef::Tuple(vec![#(#field_exprs),*])
685                })
686            }
687        }
688        syn::Fields::Unit => {
689            // Unit struct becomes null
690            Ok(quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Null) })
691        }
692    }
693}
694
695/// Convert a Rust type to its TypeScript TypeDef representation.
696/// Uses TypeScript trait for types that implement it.
697fn type_to_typedef(ty: &Type) -> TokenStream2 {
698    quote! { <#ty as ferro_type::TypeScript>::typescript() }
699}
700
701/// Generate implementation for a transparent newtype wrapper.
702/// The TypeScript representation is just the inner type, not wrapped in Named.
703fn generate_transparent_impl(
704    name: &Ident,
705    inner_type: &Type,
706    generics: &Generics,
707) -> syn::Result<TokenStream2> {
708    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
709
710    // Add TypeScript bounds to generic parameters
711    let where_clause = if generics.params.is_empty() {
712        where_clause.cloned()
713    } else {
714        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
715            if let GenericParam::Type(tp) = p {
716                Some(&tp.ident)
717            } else {
718                None
719            }
720        }).collect();
721
722        if type_params.is_empty() {
723            where_clause.cloned()
724        } else {
725            let bounds = type_params.iter().map(|p| {
726                quote! { #p: ferro_type::TypeScript }
727            });
728
729            if let Some(existing_where) = where_clause {
730                let existing_predicates = &existing_where.predicates;
731                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
732            } else {
733                Some(syn::parse_quote! { where #(#bounds),* })
734            }
735        }
736    };
737
738    Ok(quote! {
739        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
740            fn typescript() -> ferro_type::TypeDef {
741                <#inner_type as ferro_type::TypeScript>::typescript()
742            }
743        }
744    })
745}
746
747fn generate_impl(
748    name: &Ident,
749    name_str: &str,
750    generics: &Generics,
751    typedef_expr: TokenStream2,
752) -> syn::Result<TokenStream2> {
753    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
754
755    // Add TypeScript bounds to generic parameters
756    let where_clause = if generics.params.is_empty() {
757        where_clause.cloned()
758    } else {
759        let type_params: Vec<_> = generics.params.iter().filter_map(|p| {
760            if let GenericParam::Type(tp) = p {
761                Some(&tp.ident)
762            } else {
763                None
764            }
765        }).collect();
766
767        if type_params.is_empty() {
768            where_clause.cloned()
769        } else {
770            let bounds = type_params.iter().map(|p| {
771                quote! { #p: ferro_type::TypeScript }
772            });
773
774            if let Some(existing_where) = where_clause {
775                let existing_predicates = &existing_where.predicates;
776                Some(syn::parse_quote! { where #(#bounds,)* #existing_predicates })
777            } else {
778                Some(syn::parse_quote! { where #(#bounds),* })
779            }
780        }
781    };
782
783    Ok(quote! {
784        impl #impl_generics ferro_type::TypeScript for #name #ty_generics #where_clause {
785            fn typescript() -> ferro_type::TypeDef {
786                ferro_type::TypeDef::Named {
787                    name: #name_str.to_string(),
788                    def: Box::new(#typedef_expr),
789                }
790            }
791        }
792    })
793}