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