1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Item, Fields, Attribute, parse_quote};
6
7fn 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 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
36fn 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 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 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 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 let defaults = vec![#(#fields_defaults),*];
176 let kv = defaults.iter().map(|d| {
177 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 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 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 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 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 let mut meta_fields = Vec::new();
258
259 for (k, v) in meta_pairs {
261 meta_fields.push(format!("{}: \"{}\"", k, v)); }
263
264 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 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 quote! { String::new() } } else {
296 quote! { #static_fields.to_string() }
297 }
298 };
299
300 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}