bindy_macro/
lib.rs

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