bindy_macro/
lib.rs

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