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