export_type/
lib.rs

1use std::path::PathBuf;
2
3use lang::TSExporter;
4use parsers::handle_export_type_parsing;
5use proc_macro::TokenStream;
6use syn::{parse_macro_input, Attribute, DeriveInput};
7
8mod case;
9mod error;
10mod exporter;
11mod lang;
12mod parsers;
13
14use error::ToCompileError;
15
16use case::*;
17use error::*;
18use exporter::*;
19
20pub(crate) static DEFAULT_EXPORT_PATH: &str = "exports";
21
22/// Derives the ExportType trait for a struct or enum, generating TypeScript type definitions.
23///
24/// # Examples
25///
26/// ```ignore
27/// #[derive(ExportType)]
28/// #[export_type(lang = "typescript", path = "types/generated")]
29/// struct User {
30///     id: i32,
31///     name: String,
32///     #[export_type(rename = "emailAddress")]
33///     email: Option<String>,
34/// }
35///
36/// #[derive(ExportType)]
37/// enum Status {
38///     Active,
39///     Inactive,
40///     Pending { reason: String },
41/// }
42/// ```
43#[proc_macro_derive(ExportType, attributes(export_type))]
44pub fn export_type(input: TokenStream) -> TokenStream {
45    let input = parse_macro_input!(input as DeriveInput);
46    match handle_export_type(input.clone()) {
47        Ok(_output) => {
48            #[cfg(not(test))]
49            if let Ok(export_path) = get_export_path_from_attrs(&input.attrs) {
50                // Generate in OUT_DIR during build
51                if let Ok(out_dir) = std::env::var("OUT_DIR") {
52                    let out_path = PathBuf::from(out_dir);
53                    let _ = create_exporter_files(out_path.join("types"));
54                }
55
56                // During normal compilation, write to target path
57                if std::env::var("CARGO_PUBLISH").is_err() && !std::env::var("OUT_DIR").is_ok() {
58                    let _ = create_exporter_files(export_path);
59                }
60            }
61            #[cfg(test)]
62            if let Ok(export_path) = get_export_path_from_attrs(&input.attrs) {
63                // Generate in OUT_DIR during build
64                let _ = create_exporter_files(export_path);
65            }
66            quote::quote! {}.into()
67        }
68        Err(e) => e.to_compile_error().into(),
69    }
70}
71
72fn handle_export_type(input: DeriveInput) -> TSTypeResult<proc_macro2::TokenStream> {
73    let lang = get_lang_from_attrs(&input.attrs)?;
74    let mut output = handle_export_type_parsing(&input, &lang)?;
75    let name = input.ident.to_string();
76    let generics = get_generics_from_attrs(&input.attrs)?;
77    output.generics = generics;
78    output.lang = lang;
79    output.name = name;
80    add_struct_or_enum(output)?;
81    Ok(quote::quote! {})
82}
83
84fn get_exporter_from_lang(
85    lang: &str,
86    output: Output,
87    generics: Vec<String>,
88) -> TSTypeResult<Box<dyn ToOutput>> {
89    match lang {
90        "typescript" | "ts" => Ok(Box::new(TSExporter::new(output, None, generics))),
91        lang => Err(TSTypeError::UnsupportedLanguage(lang.to_string())),
92    }
93}
94
95fn get_generics_from_attrs(attrs: &[Attribute]) -> TSTypeResult<Vec<String>> {
96    let mut generics = vec![];
97
98    for attr in attrs {
99        if attr.path().is_ident("export_type") {
100            if let Ok(nested) = attr.parse_args_with(
101                syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
102            ) {
103                for meta in nested {
104                    if let syn::Meta::NameValue(nv) = meta {
105                        if nv.path.is_ident("generics") {
106                            if let syn::Expr::Lit(syn::ExprLit {
107                                lit: syn::Lit::Str(lit_str),
108                                ..
109                            }) = nv.value
110                            {
111                                generics = lit_str
112                                    .value()
113                                    .split(',')
114                                    .map(|s| s.trim().to_string())
115                                    .collect();
116                            }
117                        }
118                    }
119                }
120            }
121        }
122    }
123
124    Ok(generics)
125}
126
127fn get_lang_from_attrs(attrs: &[Attribute]) -> TSTypeResult<String> {
128    let mut lang = String::from("typescript");
129
130    for attr in attrs {
131        if attr.path().is_ident("export_type") {
132            if let Ok(nested) = attr.parse_args_with(
133                syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
134            ) {
135                for meta in nested {
136                    if let syn::Meta::NameValue(nv) = meta {
137                        if nv.path.is_ident("lang") {
138                            if let syn::Expr::Lit(syn::ExprLit {
139                                lit: syn::Lit::Str(lit_str),
140                                ..
141                            }) = nv.value
142                            {
143                                lang = lit_str.value();
144                            }
145                        }
146                    }
147                }
148            }
149        }
150    }
151
152    Ok(lang)
153}
154
155fn get_export_path_from_attrs(attrs: &[Attribute]) -> TSTypeResult<PathBuf> {
156    let mut export_path = PathBuf::from(DEFAULT_EXPORT_PATH);
157
158    for attr in attrs {
159        if attr.path().is_ident("export_type") {
160            if let Ok(nested) = attr.parse_args_with(
161                syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
162            ) {
163                for meta in nested {
164                    if let syn::Meta::NameValue(nv) = meta {
165                        if nv.path.is_ident("path") {
166                            if let syn::Expr::Lit(syn::ExprLit {
167                                lit: syn::Lit::Str(lit_str),
168                                ..
169                            }) = nv.value
170                            {
171                                export_path = PathBuf::from(lit_str.value());
172                            }
173                        }
174                    }
175                }
176            }
177        }
178    }
179
180    Ok(export_path)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use syn::parse_quote;
187
188    #[test]
189    fn test_get_lang_from_attrs() {
190        let attrs = vec![];
191        assert_eq!(
192            get_lang_from_attrs(&attrs).unwrap(),
193            "typescript".to_string()
194        );
195
196        let attrs = vec![parse_quote! { #[export_type(lang = "typescript")] }];
197        assert_eq!(
198            get_lang_from_attrs(&attrs).unwrap(),
199            "typescript".to_string()
200        );
201
202        let attrs = vec![parse_quote! { #[export_type(lang = "unsupported")] }];
203        assert_eq!(
204            get_lang_from_attrs(&attrs).unwrap(),
205            "unsupported".to_string()
206        );
207    }
208
209    #[test]
210    fn test_get_export_path_from_attrs() {
211        let attrs = vec![];
212        assert_eq!(
213            get_export_path_from_attrs(&attrs).unwrap(),
214            PathBuf::from("exports")
215        );
216
217        let attrs = vec![parse_quote! { #[export_type(path = "test")] }];
218        assert_eq!(
219            get_export_path_from_attrs(&attrs).unwrap(),
220            PathBuf::from("test")
221        );
222    }
223}