bindy_macro/
lib.rs

1use std::{fs, path::Path};
2
3use chia_sdk_bindings::CONSTANTS;
4use convert_case::{Case, Casing};
5use indexmap::IndexMap;
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::quote;
9use serde::{Deserialize, Serialize};
10use syn::{parse_str, Ident, LitStr, Type};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13struct Bindy {
14    entrypoint: String,
15    pymodule: String,
16    #[serde(default)]
17    type_groups: IndexMap<String, Vec<String>>,
18    #[serde(default)]
19    shared: IndexMap<String, String>,
20    #[serde(default)]
21    napi: IndexMap<String, String>,
22    #[serde(default)]
23    wasm: IndexMap<String, String>,
24    #[serde(default)]
25    pyo3: IndexMap<String, String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type", rename_all = "snake_case")]
30enum Binding {
31    Class {
32        #[serde(default)]
33        new: bool,
34        #[serde(default)]
35        fields: IndexMap<String, String>,
36        #[serde(default)]
37        methods: IndexMap<String, Method>,
38        #[serde(default)]
39        remote: bool,
40    },
41    Enum {
42        values: Vec<String>,
43    },
44    Function {
45        #[serde(default)]
46        args: IndexMap<String, String>,
47        #[serde(rename = "return")]
48        ret: Option<String>,
49    },
50}
51
52#[derive(Debug, Default, Clone, Serialize, Deserialize)]
53#[serde(default)]
54struct Method {
55    #[serde(rename = "type")]
56    kind: MethodKind,
57    args: IndexMap<String, String>,
58    #[serde(rename = "return")]
59    ret: Option<String>,
60}
61
62#[derive(Debug, Default, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64enum MethodKind {
65    #[default]
66    Normal,
67    Async,
68    ToString,
69    Static,
70    Factory,
71    Constructor,
72}
73
74fn load_bindings(path: &str) -> (Bindy, IndexMap<String, Binding>) {
75    let source = fs::read_to_string(path).unwrap();
76    let bindy: Bindy = serde_json::from_str(&source).unwrap();
77
78    let mut bindings = IndexMap::new();
79
80    let mut dir: Vec<_> = fs::read_dir(Path::new(path).parent().unwrap().join("bindings"))
81        .unwrap()
82        .map(|p| p.unwrap())
83        .collect();
84
85    dir.sort_by_key(|p| p.path().file_name().unwrap().to_str().unwrap().to_string());
86
87    for path in dir {
88        if path.path().extension().unwrap() == "json" {
89            let source = fs::read_to_string(path.path()).unwrap();
90            let contents: IndexMap<String, Binding> = serde_json::from_str(&source).unwrap();
91            bindings.extend(contents);
92        }
93    }
94
95    if let Binding::Class { methods, .. } =
96        &mut bindings.get_mut("Constants").expect("Constants not found")
97    {
98        for &name in CONSTANTS {
99            methods.insert(
100                name.to_string(),
101                Method {
102                    kind: MethodKind::Static,
103                    args: IndexMap::new(),
104                    ret: Some("SerializedProgram".to_string()),
105                },
106            );
107
108            methods.insert(
109                format!("{name}_hash"),
110                Method {
111                    kind: MethodKind::Static,
112                    args: IndexMap::new(),
113                    ret: Some("TreeHash".to_string()),
114                },
115            );
116        }
117    }
118
119    if let Binding::Class { methods, .. } = &mut bindings.get_mut("Clvm").expect("Clvm not found") {
120        for &name in CONSTANTS {
121            methods.insert(
122                name.to_string(),
123                Method {
124                    kind: MethodKind::Normal,
125                    args: IndexMap::new(),
126                    ret: Some("Program".to_string()),
127                },
128            );
129        }
130    }
131
132    (bindy, bindings)
133}
134
135fn build_base_mappings(bindy: &Bindy, mappings: &mut IndexMap<String, String>) {
136    for (name, value) in &bindy.shared {
137        if !mappings.contains_key(name) {
138            mappings.insert(name.clone(), value.clone());
139        }
140    }
141
142    for (name, group) in &bindy.type_groups {
143        if let Some(value) = mappings.shift_remove(name) {
144            for ty in group {
145                mappings.insert(ty.clone(), value.clone());
146            }
147        }
148    }
149}
150
151#[proc_macro]
152pub fn bindy_napi(input: TokenStream) -> TokenStream {
153    let input = syn::parse_macro_input!(input as LitStr).value();
154    let (bindy, bindings) = load_bindings(&input);
155
156    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
157
158    let mut base_mappings = bindy.napi.clone();
159    build_base_mappings(&bindy, &mut base_mappings);
160
161    let mut non_async_param_mappings = base_mappings.clone();
162    let mut async_param_mappings = base_mappings.clone();
163    let mut return_mappings = base_mappings;
164
165    for (name, binding) in &bindings {
166        if matches!(binding, Binding::Class { .. }) {
167            non_async_param_mappings.insert(
168                name.clone(),
169                format!("napi::bindgen_prelude::ClassInstance<{name}>"),
170            );
171            async_param_mappings.insert(
172                name.clone(),
173                format!("napi::bindgen_prelude::Reference<{name}>"),
174            );
175        }
176    }
177
178    // We accept Uint8Array as parameters for flexibility, but return Buffer for ease of use
179    // For context, Buffer is a subclass of Uint8Array with more methods, and is commonly used in Node.js
180    for ty in return_mappings.values_mut() {
181        if ty.as_str() == "napi::bindgen_prelude::Uint8Array" {
182            *ty = "napi::bindgen_prelude::Buffer".to_string();
183        }
184    }
185
186    let mut output = quote!();
187
188    for (name, binding) in bindings {
189        match binding {
190            Binding::Class {
191                new,
192                remote,
193                methods,
194                fields,
195            } => {
196                let bound_ident = Ident::new(&name, Span::mixed_site());
197                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
198                let fully_qualified_ident = if remote {
199                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
200                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
201                } else {
202                    quote!( #rust_struct_ident )
203                };
204
205                let mut method_tokens = quote! {
206                    #[napi]
207                    pub fn clone(&self) -> Self {
208                        Clone::clone(self)
209                    }
210                };
211
212                for (name, method) in methods {
213                    let method_ident = Ident::new(&name, Span::mixed_site());
214
215                    let param_mappings = if matches!(method.kind, MethodKind::Async) {
216                        &async_param_mappings
217                    } else {
218                        &non_async_param_mappings
219                    };
220
221                    let arg_idents = method
222                        .args
223                        .keys()
224                        .map(|k| Ident::new(k, Span::mixed_site()))
225                        .collect::<Vec<_>>();
226
227                    let arg_types = method
228                        .args
229                        .values()
230                        .map(|v| {
231                            parse_str::<Type>(apply_mappings(v, param_mappings).as_str()).unwrap()
232                        })
233                        .collect::<Vec<_>>();
234
235                    let ret = parse_str::<Type>(
236                        apply_mappings(
237                            method.ret.as_deref().unwrap_or(
238                                if matches!(
239                                    method.kind,
240                                    MethodKind::Constructor | MethodKind::Factory
241                                ) {
242                                    "Self"
243                                } else {
244                                    "()"
245                                },
246                            ),
247                            &return_mappings,
248                        )
249                        .as_str(),
250                    )
251                    .unwrap();
252
253                    let napi_attr = match method.kind {
254                        MethodKind::Constructor => quote!(#[napi(constructor)]),
255                        MethodKind::Static => quote!(#[napi]),
256                        MethodKind::Factory => quote!(#[napi(factory)]),
257                        MethodKind::Normal | MethodKind::Async | MethodKind::ToString => {
258                            quote!(#[napi])
259                        }
260                    };
261
262                    match method.kind {
263                        MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
264                            method_tokens.extend(quote! {
265                                #napi_attr
266                                pub fn #method_ident(
267                                    env: Env,
268                                    #( #arg_idents: #arg_types ),*
269                                ) -> napi::Result<#ret> {
270                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
271                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
272                                    )?, &bindy::NapiReturnContext(env))?)
273                                }
274                            });
275                        }
276                        MethodKind::Normal | MethodKind::ToString => {
277                            method_tokens.extend(quote! {
278                                #napi_attr
279                                pub fn #method_ident(
280                                    &self,
281                                    env: Env,
282                                    #( #arg_idents: #arg_types ),*
283                                ) -> napi::Result<#ret> {
284                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
285                                        &self.0,
286                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
287                                    )?, &bindy::NapiReturnContext(env))?)
288                                }
289                            });
290                        }
291                        MethodKind::Async => {
292                            method_tokens.extend(quote! {
293                                #napi_attr
294                                pub async fn #method_ident(
295                                    &self,
296                                    #( #arg_idents: #arg_types ),*
297                                ) -> napi::Result<#ret> {
298                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#method_ident(
299                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
300                                    ).await?, &bindy::NapiAsyncReturnContext)?)
301                                }
302                            });
303                        }
304                    }
305                }
306
307                let mut field_tokens = quote!();
308
309                for (name, ty) in &fields {
310                    let ident = Ident::new(name, Span::mixed_site());
311                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
312                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
313                    let get_ty =
314                        parse_str::<Type>(apply_mappings(ty, &return_mappings).as_str()).unwrap();
315                    let set_ty =
316                        parse_str::<Type>(apply_mappings(ty, &non_async_param_mappings).as_str())
317                            .unwrap();
318
319                    field_tokens.extend(quote! {
320                        #[napi(getter)]
321                        pub fn #get_ident(&self, env: Env) -> napi::Result<#get_ty> {
322                            Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#ident.clone(), &bindy::NapiReturnContext(env))?)
323                        }
324
325                        #[napi(setter)]
326                        pub fn #set_ident(&mut self, env: Env, value: #set_ty) -> napi::Result<()> {
327                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Napi>::into_rust(value, &bindy::NapiParamContext)?;
328                            Ok(())
329                        }
330                    });
331                }
332
333                if new {
334                    let arg_idents = fields
335                        .keys()
336                        .map(|k| Ident::new(k, Span::mixed_site()))
337                        .collect::<Vec<_>>();
338
339                    let arg_types = fields
340                        .values()
341                        .map(|v| {
342                            parse_str::<Type>(apply_mappings(v, &non_async_param_mappings).as_str())
343                                .unwrap()
344                        })
345                        .collect::<Vec<_>>();
346
347                    method_tokens.extend(quote! {
348                        #[napi(constructor)]
349                        pub fn new(
350                            env: Env,
351                            #( #arg_idents: #arg_types ),*
352                        ) -> napi::Result<Self> {
353                            Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#rust_struct_ident {
354                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)?),*
355                            }, &bindy::NapiReturnContext(env))?)
356                        }
357                    });
358                }
359
360                output.extend(quote! {
361                    #[napi_derive::napi]
362                    #[derive(Clone)]
363                    pub struct #bound_ident(#rust_struct_ident);
364
365                    #[napi_derive::napi]
366                    impl #bound_ident {
367                        #method_tokens
368                        #field_tokens
369                    }
370
371                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
372                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
373                            Ok(Self(value))
374                        }
375                    }
376
377                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
378                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
379                            Ok(self.0)
380                        }
381                    }
382                });
383            }
384            Binding::Enum { values } => {
385                let bound_ident = Ident::new(&name, Span::mixed_site());
386                let rust_ident = quote!( #entrypoint::#bound_ident );
387
388                let value_idents = values
389                    .iter()
390                    .map(|v| Ident::new(v, Span::mixed_site()))
391                    .collect::<Vec<_>>();
392
393                output.extend(quote! {
394                    #[napi_derive::napi]
395                    pub enum #bound_ident {
396                        #( #value_idents ),*
397                    }
398
399                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Napi> for #bound_ident {
400                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
401                            Ok(match value {
402                                #( #rust_ident::#value_idents => Self::#value_idents ),*
403                            })
404                        }
405                    }
406
407                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Napi> for #bound_ident {
408                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
409                            Ok(match self {
410                                #( Self::#value_idents => #rust_ident::#value_idents ),*
411                            })
412                        }
413                    }
414                });
415            }
416            Binding::Function { args, ret } => {
417                let bound_ident = Ident::new(&name, Span::mixed_site());
418                let ident = Ident::new(&name, Span::mixed_site());
419
420                let arg_idents = args
421                    .keys()
422                    .map(|k| Ident::new(k, Span::mixed_site()))
423                    .collect::<Vec<_>>();
424
425                let arg_types = args
426                    .values()
427                    .map(|v| {
428                        parse_str::<Type>(apply_mappings(v, &non_async_param_mappings).as_str())
429                            .unwrap()
430                    })
431                    .collect::<Vec<_>>();
432
433                let ret = parse_str::<Type>(
434                    apply_mappings(ret.as_deref().unwrap_or("()"), &return_mappings).as_str(),
435                )
436                .unwrap();
437
438                output.extend(quote! {
439                    #[napi_derive::napi]
440                    pub fn #bound_ident(
441                        env: Env,
442                        #( #arg_idents: #arg_types ),*
443                    ) -> napi::Result<#ret> {
444                        Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#entrypoint::#ident(
445                            #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
446                        )?, &bindy::NapiReturnContext(env))?)
447                    }
448                });
449            }
450        }
451    }
452
453    output.into()
454}
455
456#[proc_macro]
457pub fn bindy_wasm(input: TokenStream) -> TokenStream {
458    let input = syn::parse_macro_input!(input as LitStr).value();
459    let (bindy, bindings) = load_bindings(&input);
460
461    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
462
463    let mut mappings = bindy.wasm.clone();
464    build_base_mappings(&bindy, &mut mappings);
465
466    let mut output = quote!();
467
468    for (name, binding) in bindings {
469        match binding {
470            Binding::Class {
471                new,
472                remote,
473                methods,
474                fields,
475            } => {
476                let bound_ident = Ident::new(&name, Span::mixed_site());
477                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
478                let fully_qualified_ident = if remote {
479                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
480                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
481                } else {
482                    quote!( #rust_struct_ident )
483                };
484
485                let mut method_tokens = quote! {
486                    #[wasm_bindgen]
487                    pub fn clone(&self) -> Self {
488                        Clone::clone(self)
489                    }
490                };
491
492                for (name, method) in methods {
493                    let js_name = name.to_case(Case::Camel);
494                    let method_ident = Ident::new(&name, Span::mixed_site());
495
496                    let arg_attrs = method
497                        .args
498                        .keys()
499                        .map(|k| {
500                            let js_name = k.to_case(Case::Camel);
501                            quote!( #[wasm_bindgen(js_name = #js_name)] )
502                        })
503                        .collect::<Vec<_>>();
504
505                    let arg_idents = method
506                        .args
507                        .keys()
508                        .map(|k| Ident::new(k, Span::mixed_site()))
509                        .collect::<Vec<_>>();
510
511                    let arg_types = method
512                        .args
513                        .values()
514                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
515                        .collect::<Vec<_>>();
516
517                    let ret = parse_str::<Type>(
518                        apply_mappings(
519                            method.ret.as_deref().unwrap_or(
520                                if matches!(
521                                    method.kind,
522                                    MethodKind::Constructor | MethodKind::Factory
523                                ) {
524                                    "Self"
525                                } else {
526                                    "()"
527                                },
528                            ),
529                            &mappings,
530                        )
531                        .as_str(),
532                    )
533                    .unwrap();
534
535                    let wasm_attr = match method.kind {
536                        MethodKind::Constructor => quote!(#[wasm_bindgen(constructor)]),
537                        _ => quote!(#[wasm_bindgen(js_name = #js_name)]),
538                    };
539
540                    match method.kind {
541                        MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
542                            method_tokens.extend(quote! {
543                                #wasm_attr
544                                pub fn #method_ident(
545                                    #( #arg_attrs #arg_idents: #arg_types ),*
546                                ) -> Result<#ret, wasm_bindgen::JsError> {
547                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
548                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
549                                    )?, &bindy::WasmContext)?)
550                                }
551                            });
552                        }
553                        MethodKind::Normal | MethodKind::ToString => {
554                            method_tokens.extend(quote! {
555                                #wasm_attr
556                                pub fn #method_ident(
557                                    &self,
558                                    #( #arg_attrs #arg_idents: #arg_types ),*
559                                ) -> Result<#ret, wasm_bindgen::JsError> {
560                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
561                                        &self.0,
562                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
563                                    )?, &bindy::WasmContext)?)
564                                }
565                            });
566                        }
567                        MethodKind::Async => {
568                            method_tokens.extend(quote! {
569                                #wasm_attr
570                                pub async fn #method_ident(
571                                    &self,
572                                    #( #arg_attrs #arg_idents: #arg_types ),*
573                                ) -> Result<#ret, wasm_bindgen::JsError> {
574                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#method_ident(
575                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
576                                    ).await?, &bindy::WasmContext)?)
577                                }
578                            });
579                        }
580                    }
581                }
582
583                let mut field_tokens = quote!();
584
585                for (name, ty) in &fields {
586                    let js_name = name.to_case(Case::Camel);
587                    let ident = Ident::new(name, Span::mixed_site());
588                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
589                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
590                    let ty = parse_str::<Type>(apply_mappings(ty, &mappings).as_str()).unwrap();
591
592                    field_tokens.extend(quote! {
593                        #[wasm_bindgen(getter, js_name = #js_name)]
594                        pub fn #get_ident(&self) -> Result<#ty, wasm_bindgen::JsError> {
595                            Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#ident.clone(), &bindy::WasmContext)?)
596                        }
597
598                        #[wasm_bindgen(setter, js_name = #js_name)]
599                        pub fn #set_ident(&mut self, value: #ty) -> Result<(), wasm_bindgen::JsError> {
600                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(value, &bindy::WasmContext)?;
601                            Ok(())
602                        }
603                    });
604                }
605
606                if new {
607                    let arg_attrs = fields
608                        .keys()
609                        .map(|k| {
610                            let js_name = k.to_case(Case::Camel);
611                            quote!( #[wasm_bindgen(js_name = #js_name)] )
612                        })
613                        .collect::<Vec<_>>();
614
615                    let arg_idents = fields
616                        .keys()
617                        .map(|k| Ident::new(k, Span::mixed_site()))
618                        .collect::<Vec<_>>();
619
620                    let arg_types = fields
621                        .values()
622                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
623                        .collect::<Vec<_>>();
624
625                    method_tokens.extend(quote! {
626                        #[wasm_bindgen(constructor)]
627                        pub fn new(
628                            #( #arg_attrs #arg_idents: #arg_types ),*
629                        ) -> Result<Self, wasm_bindgen::JsError> {
630                            Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#rust_struct_ident {
631                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)?),*
632                            }, &bindy::WasmContext)?)
633                        }
634                    });
635                }
636
637                output.extend(quote! {
638                    #[wasm_bindgen::prelude::wasm_bindgen]
639                    #[derive(Clone)]
640                    pub struct #bound_ident(#rust_struct_ident);
641
642                    #[wasm_bindgen::prelude::wasm_bindgen]
643                    impl #bound_ident {
644                        #method_tokens
645                        #field_tokens
646                    }
647
648                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
649                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
650                            Ok(Self(value))
651                        }
652                    }
653
654                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
655                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
656                            Ok(self.0)
657                        }
658                    }
659                });
660            }
661            Binding::Enum { values } => {
662                let bound_ident = Ident::new(&name, Span::mixed_site());
663                let rust_ident = quote!( #entrypoint::#bound_ident );
664
665                let value_idents = values
666                    .iter()
667                    .map(|v| Ident::new(v, Span::mixed_site()))
668                    .collect::<Vec<_>>();
669
670                output.extend(quote! {
671                    #[wasm_bindgen::prelude::wasm_bindgen]
672                    #[derive(Clone)]
673                    pub enum #bound_ident {
674                        #( #value_idents ),*
675                    }
676
677                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
678                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
679                            Ok(match value {
680                                #( #rust_ident::#value_idents => Self::#value_idents ),*
681                            })
682                        }
683                    }
684
685                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
686                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
687                            Ok(match self {
688                                #( Self::#value_idents => #rust_ident::#value_idents ),*
689                            })
690                        }
691                    }
692                });
693            }
694            Binding::Function { args, ret } => {
695                let bound_ident = Ident::new(&name, Span::mixed_site());
696                let ident = Ident::new(&name, Span::mixed_site());
697
698                let js_name = name.to_case(Case::Camel);
699
700                let arg_attrs = args
701                    .keys()
702                    .map(|k| {
703                        let js_name = k.to_case(Case::Camel);
704                        quote!( #[wasm_bindgen(js_name = #js_name)] )
705                    })
706                    .collect::<Vec<_>>();
707
708                let arg_idents = args
709                    .keys()
710                    .map(|k| Ident::new(k, Span::mixed_site()))
711                    .collect::<Vec<_>>();
712
713                let arg_types = args
714                    .values()
715                    .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
716                    .collect::<Vec<_>>();
717
718                let ret = parse_str::<Type>(
719                    apply_mappings(ret.as_deref().unwrap_or("()"), &mappings).as_str(),
720                )
721                .unwrap();
722
723                output.extend(quote! {
724                    #[wasm_bindgen::prelude::wasm_bindgen(js_name = #js_name)]
725                    pub fn #bound_ident(
726                        #( #arg_attrs #arg_idents: #arg_types ),*
727                    ) -> Result<#ret, wasm_bindgen::JsError> {
728                        Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#entrypoint::#ident(
729                            #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
730                        )?, &bindy::WasmContext)?)
731                    }
732                });
733            }
734        }
735    }
736
737    output.into()
738}
739
740#[proc_macro]
741pub fn bindy_pyo3(input: TokenStream) -> TokenStream {
742    let input = syn::parse_macro_input!(input as LitStr).value();
743    let (bindy, bindings) = load_bindings(&input);
744
745    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
746
747    let mut mappings = bindy.pyo3.clone();
748    build_base_mappings(&bindy, &mut mappings);
749
750    let mut output = quote!();
751    let mut module = quote!();
752
753    for (name, binding) in bindings {
754        let bound_ident = Ident::new(&name, Span::mixed_site());
755
756        match &binding {
757            Binding::Class {
758                new,
759                remote,
760                methods,
761                fields,
762            } => {
763                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
764                let fully_qualified_ident = if *remote {
765                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
766                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
767                } else {
768                    quote!( #rust_struct_ident )
769                };
770
771                let mut method_tokens = quote! {
772                    pub fn clone(&self) -> Self {
773                        Clone::clone(self)
774                    }
775                };
776
777                for (name, method) in methods {
778                    let method_ident = Ident::new(name, Span::mixed_site());
779
780                    let arg_idents = method
781                        .args
782                        .keys()
783                        .map(|k| Ident::new(k, Span::mixed_site()))
784                        .collect::<Vec<_>>();
785
786                    let arg_types = method
787                        .args
788                        .values()
789                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
790                        .collect::<Vec<_>>();
791
792                    let ret = parse_str::<Type>(
793                        apply_mappings(
794                            method.ret.as_deref().unwrap_or(
795                                if matches!(
796                                    method.kind,
797                                    MethodKind::Constructor | MethodKind::Factory
798                                ) {
799                                    "Self"
800                                } else {
801                                    "()"
802                                },
803                            ),
804                            &mappings,
805                        )
806                        .as_str(),
807                    )
808                    .unwrap();
809
810                    let mut pyo3_attr = match method.kind {
811                        MethodKind::Constructor => quote!(#[new]),
812                        MethodKind::Static => quote!(#[staticmethod]),
813                        MethodKind::Factory => quote!(#[staticmethod]),
814                        _ => quote!(),
815                    };
816
817                    if !matches!(method.kind, MethodKind::ToString) {
818                        pyo3_attr = quote! {
819                            #pyo3_attr
820                            #[pyo3(signature = (#(#arg_idents),*))]
821                        };
822                    }
823
824                    let remapped_method_ident = if matches!(method.kind, MethodKind::ToString) {
825                        Ident::new("__str__", Span::mixed_site())
826                    } else {
827                        method_ident.clone()
828                    };
829
830                    match method.kind {
831                        MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
832                            method_tokens.extend(quote! {
833                                #pyo3_attr
834                                pub fn #remapped_method_ident(
835                                    #( #arg_idents: #arg_types ),*
836                                ) -> pyo3::PyResult<#ret> {
837                                    Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
838                                        #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
839                                    )?, &bindy::Pyo3Context)?)
840                                }
841                            });
842                        }
843                        MethodKind::Normal | MethodKind::ToString => {
844                            method_tokens.extend(quote! {
845                                #pyo3_attr
846                                pub fn #remapped_method_ident(
847                                    &self,
848                                    #( #arg_idents: #arg_types ),*
849                                ) -> pyo3::PyResult<#ret> {
850                                    Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
851                                        &self.0,
852                                        #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
853                                    )?, &bindy::Pyo3Context)?)
854                                }
855                            });
856                        }
857                        MethodKind::Async => {
858                            method_tokens.extend(quote! {
859                                #pyo3_attr
860                                pub fn #remapped_method_ident<'a>(
861                                    &self,
862                                    py: Python<'a>,
863                                    #( #arg_idents: #arg_types ),*
864                                ) -> pyo3::PyResult<pyo3::Bound<'a, pyo3::PyAny>> {
865                                    let clone_of_self = self.0.clone();
866                                    #( let #arg_idents = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?; )*
867
868                                    pyo3_async_runtimes::tokio::future_into_py(py, async move {
869                                        let result: pyo3::PyResult<#ret> = Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(clone_of_self.#method_ident(
870                                            #( #arg_idents ),*
871                                        ).await?, &bindy::Pyo3Context)?);
872                                        result
873                                    })
874                                }
875                            });
876                        }
877                    }
878                }
879
880                let mut field_tokens = quote!();
881
882                for (name, ty) in fields {
883                    let ident = Ident::new(name, Span::mixed_site());
884                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
885                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
886                    let ty = parse_str::<Type>(apply_mappings(ty, &mappings).as_str()).unwrap();
887
888                    field_tokens.extend(quote! {
889                        #[getter(#ident)]
890                        pub fn #get_ident(&self) -> pyo3::PyResult<#ty> {
891                            Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(self.0.#ident.clone(), &bindy::Pyo3Context)?)
892                        }
893
894                        #[setter(#ident)]
895                        pub fn #set_ident(&mut self, value: #ty) -> pyo3::PyResult<()> {
896                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(value, &bindy::Pyo3Context)?;
897                            Ok(())
898                        }
899                    });
900                }
901
902                if *new {
903                    let arg_idents = fields
904                        .keys()
905                        .map(|k| Ident::new(k, Span::mixed_site()))
906                        .collect::<Vec<_>>();
907
908                    let arg_types = fields
909                        .values()
910                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
911                        .collect::<Vec<_>>();
912
913                    method_tokens.extend(quote! {
914                        #[new]
915                        #[pyo3(signature = (#(#arg_idents),*))]
916                        pub fn new(
917                            #( #arg_idents: #arg_types ),*
918                        ) -> pyo3::PyResult<Self> {
919                            Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#rust_struct_ident {
920                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?),*
921                            }, &bindy::Pyo3Context)?)
922                        }
923                    });
924                }
925
926                output.extend(quote! {
927                    #[pyo3::pyclass]
928                    #[derive(Clone)]
929                    pub struct #bound_ident(#rust_struct_ident);
930
931                    #[pyo3::pymethods]
932                    impl #bound_ident {
933                        #method_tokens
934                        #field_tokens
935                    }
936
937                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
938                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
939                            Ok(Self(value))
940                        }
941                    }
942
943                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
944                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
945                            Ok(self.0)
946                        }
947                    }
948                });
949            }
950            Binding::Enum { values } => {
951                let bound_ident = Ident::new(&name, Span::mixed_site());
952                let rust_ident = quote!( #entrypoint::#bound_ident );
953
954                let value_idents = values
955                    .iter()
956                    .map(|v| Ident::new(v, Span::mixed_site()))
957                    .collect::<Vec<_>>();
958
959                output.extend(quote! {
960                    #[pyo3::pyclass(eq, eq_int)]
961                    #[derive(Clone, PartialEq, Eq)]
962                    pub enum #bound_ident {
963                        #( #value_idents ),*
964                    }
965
966                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
967                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
968                            Ok(match value {
969                                #( #rust_ident::#value_idents => Self::#value_idents ),*
970                            })
971                        }
972                    }
973
974                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
975                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
976                            Ok(match self {
977                                #( Self::#value_idents => #rust_ident::#value_idents ),*
978                            })
979                        }
980                    }
981                });
982            }
983            Binding::Function { args, ret } => {
984                let arg_idents = args
985                    .keys()
986                    .map(|k| Ident::new(k, Span::mixed_site()))
987                    .collect::<Vec<_>>();
988
989                let arg_types = args
990                    .values()
991                    .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
992                    .collect::<Vec<_>>();
993
994                let ret = parse_str::<Type>(
995                    apply_mappings(ret.as_deref().unwrap_or("()"), &mappings).as_str(),
996                )
997                .unwrap();
998
999                output.extend(quote! {
1000                    #[pyo3::pyfunction]
1001                    #[pyo3(signature = (#(#arg_idents),*))]
1002                    pub fn #bound_ident(
1003                        #( #arg_idents: #arg_types ),*
1004                    ) -> pyo3::PyResult<#ret> {
1005                        Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#entrypoint::#bound_ident(
1006                            #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
1007                        )?, &bindy::Pyo3Context)?)
1008                    }
1009                });
1010            }
1011        }
1012
1013        match binding {
1014            Binding::Class { .. } => {
1015                module.extend(quote! {
1016                    m.add_class::<#bound_ident>()?;
1017                });
1018            }
1019            Binding::Enum { .. } => {
1020                module.extend(quote! {
1021                    m.add_class::<#bound_ident>()?;
1022                });
1023            }
1024            Binding::Function { .. } => {
1025                module.extend(quote! {
1026                    m.add_function(pyo3::wrap_pyfunction!(#bound_ident, m)?)?;
1027                });
1028            }
1029        }
1030    }
1031
1032    let pymodule = Ident::new(&bindy.pymodule, Span::mixed_site());
1033
1034    output.extend(quote! {
1035        #[pyo3::pymodule]
1036        fn #pymodule(m: &pyo3::Bound<'_, pyo3::prelude::PyModule>) -> pyo3::PyResult<()> {
1037            use pyo3::types::PyModuleMethods;
1038            #module
1039            Ok(())
1040        }
1041    });
1042
1043    output.into()
1044}
1045
1046fn apply_mappings(ty: &str, mappings: &IndexMap<String, String>) -> String {
1047    // First check if the entire type has a direct mapping
1048    if let Some(mapped) = mappings.get(ty) {
1049        return mapped.clone();
1050    }
1051
1052    // Check if this is a generic type by looking for < and >
1053    if let (Some(start), Some(end)) = (ty.find('<'), ty.rfind('>')) {
1054        let base_type = &ty[..start];
1055        let generic_part = &ty[start + 1..end];
1056
1057        // Split generic parameters by comma and trim whitespace
1058        let generic_params: Vec<&str> = generic_part.split(',').map(|s| s.trim()).collect();
1059
1060        // Recursively apply mappings to each generic parameter
1061        let mapped_params: Vec<String> = generic_params
1062            .into_iter()
1063            .map(|param| apply_mappings(param, mappings))
1064            .collect();
1065
1066        // Check if the base type needs mapping
1067        let mapped_base = mappings
1068            .get(base_type)
1069            .map(|s| s.as_str())
1070            .unwrap_or(base_type);
1071
1072        // Reconstruct the type with mapped components
1073        format!("{}<{}>", mapped_base, mapped_params.join(", "))
1074    } else {
1075        // No generics, return original if no mapping exists
1076        ty.to_string()
1077    }
1078}