bindy_macro/
lib.rs

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