Skip to main content

to_ts_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Item, Fields, Attribute, parse_quote};
6
7/// 从 doc 注释中提取键值对
8/// 格式: /// label = "低优先级", icon = "🔵"
9fn parse_doc_meta(attrs: &[Attribute]) -> Vec<(String, String)> {
10    let mut result = Vec::new();
11    for attr in attrs {
12        if attr.path().is_ident("doc") {
13            if let syn::Meta::NameValue(meta) = &attr.meta {
14                if let syn::Expr::Lit(expr_lit) = &meta.value {
15                    if let syn::Lit::Str(lit_str) = &expr_lit.lit {
16                        let doc = lit_str.value();
17                        let doc = doc.trim();
18                        // 解析 key = "value" 对
19                        for part in doc.split(',') {
20                            let part = part.trim();
21                            if let Some(eq_pos) = part.find('=') {
22                                let key = part[..eq_pos].trim().to_string();
23                                let value = part[eq_pos + 1..].trim();
24                                let value = value.trim_matches('"').to_string();
25                                result.push((key, value));
26                            }
27                        }
28                    }
29                }
30            }
31        }
32    }
33    result
34}
35
36/// 将 CamelCase 转换为 snake_case
37fn to_snake_case(s: &str) -> String {
38    let mut result = String::new();
39    for (i, c) in s.char_indices() {
40        if c.is_uppercase() {
41            if i > 0 {
42                result.push('_');
43            }
44            result.push(c.to_ascii_lowercase());
45        } else {
46            result.push(c);
47        }
48    }
49    result
50}
51fn to_ts_value(expr: &syn::Expr) -> String {
52    match expr {
53        syn::Expr::Lit(expr_lit) => {
54             match &expr_lit.lit {
55                 syn::Lit::Str(s) => format!("\"{}\"", s.value()),
56                 syn::Lit::Char(c) => format!("\"{}\"", c.value()),
57                 syn::Lit::Int(i) => i.base10_digits().to_string(),
58                 syn::Lit::Float(f) => f.base10_digits().to_string(),
59                 syn::Lit::Bool(b) => b.value.to_string(),
60                 _ => quote::quote!(#expr).to_string(),
61             }
62        },
63        syn::Expr::Array(expr_array) => {
64            let elems: Vec<String> = expr_array.elems.iter().map(to_ts_value).collect();
65            format!("[{}]", elems.join(", "))
66        },
67        syn::Expr::Struct(expr_struct) => {
68            let fields: Vec<String> = expr_struct.fields.iter().map(|field| {
69                 let key = match &field.member {
70                     syn::Member::Named(ident) => ident.to_string(),
71                     syn::Member::Unnamed(idx) => idx.index.to_string(),
72                 };
73                 let val = to_ts_value(&field.expr);
74                 format!("{}: {}", key, val)
75            }).collect();
76            format!("{{ {} }}", fields.join(", "))
77        },
78        syn::Expr::Call(expr_call) => {
79            // Check for specific constructors
80            if let syn::Expr::Path(path) = &*expr_call.func {
81                if let Some(last_segment) = path.path.segments.last() {
82                     if last_segment.ident == "new" {
83                         let path_str = quote::quote!(#path).to_string();
84                         // Remove spaces for easier matching
85                         let path_str = path_str.replace(" ", "");
86                         if path_str.contains("Vec::new") || path_str.contains("VecDeque::new") {
87                             return "[]".to_string();
88                         }
89                         if path_str.contains("String::new") {
90                             return "\"\"".to_string();
91                         }
92                     }
93                }
94            }
95             quote::quote!(#expr).to_string()
96        },
97        _ => quote::quote!(#expr).to_string()
98    }
99}
100
101#[proc_macro_attribute]
102pub fn ts(attr: TokenStream, item: TokenStream) -> TokenStream {
103    let input = parse_macro_input!(item as Item);
104    
105    let attr_str = attr.to_string();
106    let enum_format = if attr_str.contains("\"union\"") {
107        "union"
108    } else if attr_str.contains("\"string\"") {
109        "string"
110    } else if attr_str.contains("\"number\"") {
111        "number"
112    } else {
113        "union"
114    };
115    
116    let generate_meta = attr_str.contains("meta");
117
118    let is_class = attr_str.contains("class");
119
120    match input {
121        Item::Struct(ref s) => {
122            let name = &s.ident;
123            let name_str = name.to_string();
124            let mut fields_ts = Vec::new();
125            let mut fields_defaults = Vec::new();
126
127            match &s.fields {
128                Fields::Named(fields) => {
129                    for field in &fields.named {
130                        let field_name = field.ident.as_ref().unwrap();
131                        let field_type = &field.ty;
132
133                        // Check for default override in doc comments
134                        let meta_pairs = parse_doc_meta(&field.attrs);
135                        let mut default_override = None;
136                        for (k, v) in meta_pairs {
137                            if k == "default" {
138                                default_override = Some(v);
139                                break;
140                            }
141                        }
142
143                        let default_val_expr = if let Some(def_val) = default_override {
144                             quote! { #def_val.to_string() }
145                        } else {
146                             quote! { <#field_type as ::to_ts::TsType>::ts_default() }
147                        };
148
149                        fields_ts.push(quote! {
150                            format!("{}: {};", stringify!(#field_name), <#field_type as ::to_ts::TsType>::ts_name())
151                        });
152
153                         fields_defaults.push(quote! {
154                            format!("{}: {}", stringify!(#field_name), #default_val_expr)
155                        });
156                    }
157                }
158                _ => panic!("Only named fields are supported for #[ts] on structs"),
159            }
160            let expanded = quote! {
161                #s
162
163                #[cfg(not(target_arch = "wasm32"))]
164                const _: () = {
165                    impl ::to_ts::TsType for #name {
166                        fn ts_name() -> String {
167                            stringify!(#name).to_string()
168                        }
169
170                        fn ts_default() -> String {
171                            if #is_class {
172                                format!("new {}()", stringify!(#name))
173                            } else {
174                                // For interfaces, return an object literal with defaults
175                                let defaults = vec![#(#fields_defaults),*];
176                                let kv = defaults.iter().map(|d| {
177                                     // d is "key: value"
178                                     d.as_str()
179                                }).collect::<Vec<_>>();
180                                format!("{{ {} }}", kv.join(", "))
181                            }
182                        }
183
184                        fn ts_definition() -> Option<String> {
185                            let fields = vec![#(#fields_ts),*];
186                            if #is_class {
187                                let defaults = vec![#(#fields_defaults),*];
188                                
189                                let class_fields = fields.iter().zip(defaults.iter()).map(|(f, d)| {
190                                    // f is "name: type;"
191                                    // d is "name: default"
192                                    // We want "name: type = default;"
193                                    let type_part = f.trim_end_matches(';');
194                                    let default_part = d.splitn(2, ':').nth(1).unwrap_or("null").trim();
195                                    format!("{} = {};", type_part, default_part)
196                                }).collect::<Vec<_>>();
197
198                                let constructor_assigns = defaults.iter().map(|d| {
199                                    let parts: Vec<&str> = d.splitn(2, ':').collect();
200                                    let key = parts[0].trim();
201                                    format!("if (init.{} !== undefined) this.{} = init.{};", key, key, key)
202                                }).collect::<Vec<_>>();
203
204                                Some(format!(
205                                    "export class {} {{\n  {}\n\n  constructor(init?: Partial<{}>) {{\n    if (!init) return;\n    {}\n  }}\n}}",
206                                    stringify!(#name),
207                                    class_fields.join("\n  "),
208                                    stringify!(#name),
209                                    constructor_assigns.join("\n    ")
210                                ))
211                            } else {
212                                Some(format!("export interface {} {{\n  {}\n}}", stringify!(#name), fields.join("\n  ")))
213                            }
214                        }
215                    }
216
217                    ::to_ts::inventory::submit! {
218                        ::to_ts::TsDefinition {
219                            name: #name_str,
220                            definition: || <#name as ::to_ts::TsType>::ts_definition(),
221                        }
222                    }
223                };
224            };
225            TokenStream::from(expanded)
226        }
227        Item::Enum(mut e) => {
228            let name = &e.ident;
229            let name_str = name.to_string();
230            
231            // 如果是数字枚举,且没有 repr 属性,自动添加 #[repr(u8)]
232            if enum_format == "number" {
233                let has_repr = e.attrs.iter().any(|attr| attr.path().is_ident("repr"));
234                if !has_repr {
235                    e.attrs.push(parse_quote!(#[repr(u8)]));
236                }
237            }
238            
239            // 收集变体的元数据
240            let mut meta_entries = Vec::new();
241            if generate_meta {
242                for variant in &e.variants {
243                    let variant_name = &variant.ident;
244                    let variant_name_str = variant_name.to_string();
245                    let mut meta_pairs = parse_doc_meta(&variant.attrs);
246                    
247                    // 自动生成 label 如果没有提供
248                    if !meta_pairs.iter().any(|(k, _)| k == "label") {
249                        meta_pairs.push(("label".to_string(), to_snake_case(&variant_name_str)));
250                    }
251
252
253
254
255                     
256                    // 重构 meta 生成逻辑以支持动态值 (如 code)
257                    let mut meta_fields = Vec::new();
258                    
259                    // 1. 添加 doc 注释中的静态元数据
260                    for (k, v) in meta_pairs {
261                        meta_fields.push(format!("{}: \"{}\"", k, v)); // 注意:这里还是 compile time string creation
262                    }
263                    
264                    // 2. 如果是数字枚举,添加 code 字段
265                    let code_field = if enum_format == "number" {
266                         let value = if let Some((_, expr)) = &variant.discriminant {
267                            quote! { #expr }
268                        } else {
269                            quote! { Self::#variant_name as isize }
270                        };
271                        Some(quote! { format!("code: {}", #value) })
272                    } else {
273                        None
274                    };
275
276                    // 组合所有字段
277                    // 我们需要构造类似 `key: "value", code: 1` 的字符串
278                    // 静态字段部分已经是字符串了,code 是动态的
279                    
280                    let static_fields = meta_fields.join(", ");
281                    
282                    let meta_obj_expr = if let Some(code_expr) = code_field {
283                         if static_fields.is_empty() {
284                             quote! { #code_expr }
285                         } else {
286                             quote! { format!("{}, {}", #static_fields, #code_expr) }
287                         }
288                    } else {
289                        if static_fields.is_empty() {
290                             // 如果没有元数据且不是数字枚举,跳过(或者生成空对象,视需求而定)
291                             // 这里为了保持一致性,如果没有元数据,就不生成这一项?
292                             // 但之前的逻辑是 !meta_pairs.is_empty() 才 push
293                             // 现在如果是 Number 枚举,即使没有 meta pairs,也要生成 code
294                             quote! { String::new() } // placeholder, handled below check
295                        } else {
296                             quote! { #static_fields.to_string() }
297                        }
298                    };
299                    
300                    // 只有当有内容时才添加
301                    if !meta_fields.is_empty() || enum_format == "number" {
302                         meta_entries.push(quote! {
303                            format!("  [{}.{}]: {{ {} }},", stringify!(#name), #variant_name_str, #meta_obj_expr)
304                        });
305                    }
306                }
307            }
308            
309            let (ts_name_impl, ts_def_impl, ts_default_impl) = match enum_format {
310                "string" => {
311                    let mut variants_ts = Vec::new();
312                    let mut first_variant = None;
313                    
314                    for variant in &e.variants {
315                        let variant_name = &variant.ident;
316                        if first_variant.is_none() {
317                            first_variant = Some(variant_name.to_string());
318                        }
319                        variants_ts.push(quote! {
320                            format!("{} = \"{}\",", stringify!(#variant_name), stringify!(#variant_name))
321                        });
322                    }
323                    
324                    let default_val = if let Some(v) = first_variant {
325                        quote! { format!("{}.{}", stringify!(#name), #v) }
326                    } else {
327                        quote! { "null".to_string() }
328                    };
329
330                    (
331                        quote! { stringify!(#name).to_string() },
332                        if generate_meta && !meta_entries.is_empty() {
333                            quote! {
334                                let variants = vec![#(#variants_ts),*];
335                                let meta = vec![#(#meta_entries),*];
336                                Some(format!("export enum {} {{\n  {}\n}}\n\nexport const {}Meta = {{\n{}\n}} as const;", 
337                                    stringify!(#name), 
338                                    variants.join("\n  "),
339                                    stringify!(#name),
340                                    meta.join("\n")))
341                            }
342                        } else {
343                            quote! {
344                                let variants = vec![#(#variants_ts),*];
345                                Some(format!("export enum {} {{\n  {}\n}}", stringify!(#name), variants.join("\n  ")))
346                            }
347                        },
348                        default_val
349                    )
350                }
351                "number" => {
352                    let mut variants_ts = Vec::new();
353                    let mut first_variant = None;
354
355                    for variant in &e.variants {
356                        let variant_name = &variant.ident;
357                        if first_variant.is_none() {
358                            first_variant = Some(variant_name.to_string());
359                        }
360                        let value = if let Some((_, expr)) = &variant.discriminant {
361                            quote! { #expr }
362                        } else {
363                            quote! { Self::#variant_name as isize }
364                        };
365                        variants_ts.push(quote! {
366                            format!("{} = {},", stringify!(#variant_name), #value)
367                        });
368                    }
369
370                    let default_val = if let Some(v) = first_variant {
371                        quote! { format!("{}.{}", stringify!(#name), #v) }
372                    } else {
373                        quote! { "null".to_string() }
374                    };
375
376                    (
377                        quote! { stringify!(#name).to_string() },
378                        if generate_meta && !meta_entries.is_empty() {
379                            quote! {
380                                let variants = vec![#(#variants_ts),*];
381                                let meta = vec![#(#meta_entries),*];
382                                Some(format!("export enum {} {{\n  {}\n}}\n\nexport const {}Meta = {{\n{}\n}} as const;", 
383                                    stringify!(#name), 
384                                    variants.join("\n  "),
385                                    stringify!(#name),
386                                    meta.join("\n")))
387                            }
388                        } else {
389                            quote! {
390                                let variants = vec![#(#variants_ts),*];
391                                Some(format!("export enum {} {{\n  {}\n}}", stringify!(#name), variants.join("\n  ")))
392                            }
393                        },
394                        default_val
395                    )
396                }
397                _ => {
398                    let mut variants_ts = Vec::new();
399                    let mut first_variant = None;
400
401                    for variant in &e.variants {
402                        let variant_name = &variant.ident;
403                        if first_variant.is_none() {
404                            first_variant = Some(variant_name.to_string());
405                        }
406                        variants_ts.push(quote! {
407                            format!("\"{}\"", stringify!(#variant_name))
408                        });
409                    }
410                    
411                    let default_val = if let Some(v) = first_variant {
412                        quote! { format!("\"{}\"", #v) }
413                    } else {
414                        quote! { "null".to_string() }
415                    };
416
417                    (
418                        quote! { stringify!(#name).to_string() },
419                        quote! {
420                            let variants = vec![#(#variants_ts),*];
421                            Some(format!("export type {} = {};", stringify!(#name), variants.join(" | ")))
422                        },
423                        default_val
424                    )
425                }
426            };
427            
428            let expanded = quote! {
429                #e
430
431                #[cfg(not(target_arch = "wasm32"))]
432                const _: () = {
433                    impl ::to_ts::TsType for #name {
434                        fn ts_name() -> String {
435                            #ts_name_impl
436                        }
437                        
438                        fn ts_default() -> String {
439                            #ts_default_impl
440                        }
441
442                        fn ts_definition() -> Option<String> {
443                            #ts_def_impl
444                        }
445                    }
446
447                    ::to_ts::inventory::submit! {
448                        ::to_ts::TsDefinition {
449                            name: #name_str,
450                            definition: || <#name as ::to_ts::TsType>::ts_definition(),
451                        }
452                    }
453                };
454            };
455            TokenStream::from(expanded)
456        }
457        Item::Const(ref c) => {
458            let name = &c.ident;
459            let name_str = name.to_string();
460            let ty = &c.ty;
461            let expr = &c.expr;
462            let dummy_name = quote::format_ident!("__TS_EXPORT_{}", name);
463            let ts_val_str = to_ts_value(expr);
464
465            let expanded = quote! {
466                #c
467
468                #[cfg(not(target_arch = "wasm32"))]
469                const _: () = {
470                    #[allow(non_camel_case_types)]
471                    pub struct #dummy_name;
472
473                    impl ::to_ts::TsType for #dummy_name {
474                        fn ts_name() -> String {
475                            stringify!(#name).to_string()
476                        }
477
478                        fn ts_definition() -> Option<String> {
479                            Some(format!("export const {}: {} = {};", stringify!(#name), <#ty as ::to_ts::TsType>::ts_name(), #ts_val_str))
480                        }
481                    }
482
483                    ::to_ts::inventory::submit! {
484                        ::to_ts::TsDefinition {
485                            name: #name_str,
486                            definition: || <#dummy_name as ::to_ts::TsType>::ts_definition(),
487                        }
488                    }
489                };
490            };
491            TokenStream::from(expanded)
492        }
493        _ => panic!("The #[ts] attribute can only be used on structs, enums, and constants"),
494    }
495}