1use 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#[derive(Debug, Clone, Copy, PartialEq)]
19enum RenameAll {
20 CamelCase,
22 PascalCase,
24 SnakeCase,
26 ScreamingSnakeCase,
28 KebabCase,
30 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
59fn 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
80fn 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
99fn 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#[derive(Default)]
115struct ContainerAttrs {
116 rename: Option<String>,
118 rename_all: Option<RenameAll>,
120 transparent: bool,
122 tag: Option<String>,
124 content: Option<String>,
126 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#[derive(Default)]
179struct FieldAttrs {
180 rename: Option<String>,
182 skip: bool,
184 flatten: bool,
186 type_override: Option<String>,
188 default: bool,
190 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
227fn get_field_name(
229 original: &str,
230 field_attrs: &FieldAttrs,
231 container_attrs: &ContainerAttrs,
232) -> String {
233 if let Some(ref renamed) = field_attrs.rename {
235 return renamed.clone();
236 }
237
238 if let Some(rename_all) = container_attrs.rename_all {
240 return rename_all.apply(original);
241 }
242
243 original.to_string()
245}
246
247#[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 let container_attrs = ContainerAttrs::from_attrs(&input.attrs)?;
308
309 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 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 let all_unit = variants.iter().all(|v| matches!(v.fields, Fields::Unit));
360
361 if container_attrs.untagged {
363 return generate_untagged_enum(variants, container_attrs);
364 }
365
366 let tag_name = container_attrs.tag.as_deref().unwrap_or("type");
368
369 let content_name = container_attrs.content.as_deref();
371
372 if all_unit {
373 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 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 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 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 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 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 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 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
523fn 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 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 let field_type = &fields.unnamed.first().unwrap().ty;
549 type_to_typedef(field_type)
550 } else {
551 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 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 if fields.named.is_empty() {
599 return Ok(quote! { ferro_type::TypeDef::Object(vec![]) });
601 }
602
603 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 if field_attrs.skip {
611 continue;
612 }
613
614 let field_type = &f.ty;
615
616 if field_attrs.flatten {
617 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 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 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 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 if fields.unnamed.len() == 1 {
671 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 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 Ok(quote! { ferro_type::TypeDef::Primitive(ferro_type::Primitive::Null) })
691 }
692 }
693}
694
695fn type_to_typedef(ty: &Type) -> TokenStream2 {
698 quote! { <#ty as ferro_type::TypeScript>::typescript() }
699}
700
701fn 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 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 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}