1use 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#[derive(Debug, Clone, Copy, PartialEq)]
20enum RenameAll {
21 CamelCase,
23 PascalCase,
25 SnakeCase,
27 ScreamingSnakeCase,
29 KebabCase,
31 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
60fn 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
81fn 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
100fn 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#[derive(Default)]
116struct ContainerAttrs {
117 rename: Option<String>,
119 rename_all: Option<RenameAll>,
121 transparent: bool,
123 tag: Option<String>,
125 content: Option<String>,
127 untagged: bool,
129 pattern: Option<String>,
131 namespace: Vec<String>,
133 extends: Option<String>,
135 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 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#[derive(Clone)]
206enum IndexSpec {
207 Type(syn::Type),
209 String(String),
211}
212
213#[derive(Clone)]
215enum KeySpec {
216 Ident(syn::Ident),
218 String(String),
220}
221
222struct IndexedAccessValidation {
224 index_type: syn::Type,
226 key_ident: syn::Ident,
228 field_index: usize,
230}
231
232#[derive(Default)]
234struct FieldAttrs {
235 rename: Option<String>,
237 skip: bool,
239 flatten: bool,
241 type_override: Option<String>,
243 default: bool,
245 inline: bool,
247 index: Option<IndexSpec>,
249 key: Option<KeySpec>,
251 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 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 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 fn has_indexed_access(&self) -> bool {
312 self.index.is_some() && self.key.is_some()
313 }
314
315 fn has_pattern(&self) -> bool {
317 self.pattern.is_some()
318 }
319}
320
321fn get_field_name(
323 original: &str,
324 field_attrs: &FieldAttrs,
325 container_attrs: &ContainerAttrs,
326) -> String {
327 if let Some(ref renamed) = field_attrs.rename {
329 return renamed.clone();
330 }
331
332 if let Some(rename_all) = container_attrs.rename_all {
334 return rename_all.apply(original);
335 }
336
337 original.to_string()
339}
340
341fn 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 strings.push(std::mem::take(&mut current));
360 chars.next(); 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 strings.push(current);
393
394 Ok((strings, types))
395}
396
397fn 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 other => {
409 let type_ref = other.trim();
410 quote! { ferro_type::TypeDef::Ref(#type_ref.to_string()) }
411 }
412 }
413}
414
415fn 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#[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(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 let container_attrs = ContainerAttrs::from_attrs(&input.attrs)?;
506
507 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 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 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 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 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 let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
586
587 if container_attrs.untagged {
589 return generate_untagged_enum(variants, container_attrs);
590 }
591
592 let tag_name = container_attrs.tag.as_deref().unwrap_or("type");
594
595 let content_name = container_attrs.content.as_deref();
597
598 if all_unit {
599 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 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 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 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 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 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 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 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
749fn 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 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 let field_type = &fields.unnamed.first().unwrap().ty;
775 type_to_typedef(field_type)
776 } else {
777 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 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 if fields.named.is_empty() {
825 return Ok((quote! { ferro_type::TypeDef::Object(vec![]) }, vec![]));
827 }
828
829 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 if field_attrs.skip {
839 continue;
840 }
841
842 let field_type = &f.ty;
843
844 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 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 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 let index_spec = field_attrs.index.as_ref().unwrap();
870 let key_spec = field_attrs.key.as_ref().unwrap();
871
872 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 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 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 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 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 if fields.unnamed.len() == 1 {
945 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 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 Ok((quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Null) }, vec![]))
965 }
966 }
967}
968
969fn 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 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
1022fn type_to_typedef(ty: &Type) -> TokenStream2 {
1025 quote! { <#ty as ferro_type::TS>::typescript() }
1026}
1027
1028fn 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 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 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 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 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 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}