bindy_macro/
lib.rs

1use std::{fs, mem, path::Path};
2
3use chia_sdk_bindings::CONSTANTS;
4use convert_case::{Case, Casing};
5use indexmap::IndexMap;
6use indoc::formatdoc;
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::quote;
10use serde::{Deserialize, Serialize};
11use syn::{parse_str, Ident, LitStr, Type};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14struct Bindy {
15    entrypoint: String,
16    pymodule: String,
17    #[serde(default)]
18    type_groups: IndexMap<String, Vec<String>>,
19    #[serde(default)]
20    shared: IndexMap<String, String>,
21    #[serde(default)]
22    napi: IndexMap<String, String>,
23    #[serde(default)]
24    wasm: IndexMap<String, String>,
25    #[serde(default)]
26    wasm_stubs: IndexMap<String, String>,
27    #[serde(default)]
28    pyo3: IndexMap<String, String>,
29    #[serde(default)]
30    pyo3_stubs: IndexMap<String, String>,
31    #[serde(default)]
32    clvm_types: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(tag = "type", rename_all = "snake_case")]
37enum Binding {
38    Class {
39        #[serde(default)]
40        new: bool,
41        #[serde(default)]
42        fields: IndexMap<String, String>,
43        #[serde(default)]
44        methods: IndexMap<String, Method>,
45        #[serde(default)]
46        remote: bool,
47    },
48    Enum {
49        values: Vec<String>,
50    },
51    Function {
52        #[serde(default)]
53        args: IndexMap<String, String>,
54        #[serde(rename = "return")]
55        ret: Option<String>,
56    },
57}
58
59#[derive(Debug, Default, Clone, Serialize, Deserialize)]
60#[serde(default)]
61struct Method {
62    #[serde(rename = "type")]
63    kind: MethodKind,
64    args: IndexMap<String, String>,
65    #[serde(rename = "return")]
66    ret: Option<String>,
67    #[serde(default)]
68    stub_only: bool,
69}
70
71#[derive(Debug, Default, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73enum MethodKind {
74    #[default]
75    Normal,
76    Async,
77    ToString,
78    Static,
79    Factory,
80    Constructor,
81}
82
83fn load_bindings(path: &str) -> (Bindy, IndexMap<String, Binding>) {
84    let source = fs::read_to_string(path).unwrap();
85    let bindy: Bindy = serde_json::from_str(&source).unwrap();
86
87    let mut bindings = IndexMap::new();
88
89    let mut dir: Vec<_> = fs::read_dir(Path::new(path).parent().unwrap().join("bindings"))
90        .unwrap()
91        .map(|p| p.unwrap())
92        .collect();
93
94    dir.sort_by_key(|p| p.path().file_name().unwrap().to_str().unwrap().to_string());
95
96    for path in dir {
97        if path.path().extension().unwrap() == "json" {
98            let source = fs::read_to_string(path.path()).unwrap();
99            let contents: IndexMap<String, Binding> = serde_json::from_str(&source).unwrap();
100            bindings.extend(contents);
101        }
102    }
103
104    if let Binding::Class { methods, .. } =
105        &mut bindings.get_mut("Constants").expect("Constants not found")
106    {
107        for &name in CONSTANTS {
108            methods.insert(
109                name.to_string(),
110                Method {
111                    kind: MethodKind::Static,
112                    args: IndexMap::new(),
113                    ret: Some("SerializedProgram".to_string()),
114                    stub_only: false,
115                },
116            );
117
118            methods.insert(
119                format!("{name}_hash"),
120                Method {
121                    kind: MethodKind::Static,
122                    args: IndexMap::new(),
123                    ret: Some("TreeHash".to_string()),
124                    stub_only: false,
125                },
126            );
127        }
128    }
129
130    if let Binding::Class { methods, .. } = &mut bindings.get_mut("Clvm").expect("Clvm not found") {
131        for &name in CONSTANTS {
132            methods.insert(
133                name.to_string(),
134                Method {
135                    kind: MethodKind::Normal,
136                    args: IndexMap::new(),
137                    ret: Some("Program".to_string()),
138                    stub_only: false,
139                },
140            );
141        }
142    }
143
144    (bindy, bindings)
145}
146
147fn build_base_mappings(
148    bindy: &Bindy,
149    mappings: &mut IndexMap<String, String>,
150    stubs: &mut IndexMap<String, String>,
151) {
152    for (name, value) in &bindy.shared {
153        if !mappings.contains_key(name) {
154            mappings.insert(name.clone(), value.clone());
155        }
156
157        if !stubs.contains_key(name) {
158            stubs.insert(name.clone(), value.clone());
159        }
160    }
161
162    for (name, group) in &bindy.type_groups {
163        if let Some(value) = stubs.shift_remove(name) {
164            for ty in group {
165                if !stubs.contains_key(ty) {
166                    stubs.insert(ty.clone(), value.clone());
167                }
168            }
169        }
170
171        if let Some(value) = mappings.shift_remove(name) {
172            for ty in group {
173                if !mappings.contains_key(ty) {
174                    mappings.insert(ty.clone(), value.clone());
175                }
176
177                if !stubs.contains_key(ty) {
178                    stubs.insert(ty.clone(), value.clone());
179                }
180            }
181        }
182    }
183}
184
185#[proc_macro]
186pub fn bindy_napi(input: TokenStream) -> TokenStream {
187    let input = syn::parse_macro_input!(input as LitStr).value();
188    let (bindy, bindings) = load_bindings(&input);
189
190    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
191
192    let mut base_mappings = bindy.napi.clone();
193    build_base_mappings(&bindy, &mut base_mappings, &mut IndexMap::new());
194
195    let mut non_async_param_mappings = base_mappings.clone();
196    let mut async_param_mappings = base_mappings.clone();
197    let mut return_mappings = base_mappings;
198
199    for (name, binding) in &bindings {
200        if matches!(binding, Binding::Class { .. }) {
201            non_async_param_mappings.insert(
202                name.clone(),
203                format!("napi::bindgen_prelude::ClassInstance<'_, {name}>"),
204            );
205            async_param_mappings.insert(name.clone(), format!("&'_ {name}"));
206        }
207    }
208
209    // We accept Uint8Array as parameters for flexibility, but return Buffer for ease of use
210    // For context, Buffer is a subclass of Uint8Array with more methods, and is commonly used in Node.js
211    for ty in return_mappings.values_mut() {
212        if ty.as_str() == "napi::bindgen_prelude::Uint8Array" {
213            *ty = "napi::bindgen_prelude::Buffer".to_string();
214        }
215    }
216
217    let mut output = quote!();
218
219    for (name, binding) in bindings {
220        match binding {
221            Binding::Class {
222                new,
223                remote,
224                methods,
225                fields,
226            } => {
227                let bound_ident = Ident::new(&name, Span::mixed_site());
228                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
229                let fully_qualified_ident = if remote {
230                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
231                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
232                } else {
233                    quote!( #rust_struct_ident )
234                };
235
236                let mut method_tokens = quote! {
237                    #[napi]
238                    pub fn clone(&self) -> Self {
239                        Clone::clone(self)
240                    }
241                };
242
243                for (name, method) in methods {
244                    if method.stub_only {
245                        continue;
246                    }
247
248                    let method_ident = Ident::new(&name, Span::mixed_site());
249
250                    let param_mappings = if matches!(method.kind, MethodKind::Async) {
251                        &async_param_mappings
252                    } else {
253                        &non_async_param_mappings
254                    };
255
256                    let arg_idents = method
257                        .args
258                        .keys()
259                        .map(|k| Ident::new(k, Span::mixed_site()))
260                        .collect::<Vec<_>>();
261
262                    let arg_types = method
263                        .args
264                        .values()
265                        .map(|v| {
266                            parse_str::<Type>(apply_mappings(v, param_mappings).as_str()).unwrap()
267                        })
268                        .collect::<Vec<_>>();
269
270                    let ret = parse_str::<Type>(
271                        apply_mappings(
272                            method.ret.as_deref().unwrap_or(
273                                if matches!(
274                                    method.kind,
275                                    MethodKind::Constructor | MethodKind::Factory
276                                ) {
277                                    "Self"
278                                } else {
279                                    "()"
280                                },
281                            ),
282                            &return_mappings,
283                        )
284                        .as_str(),
285                    )
286                    .unwrap();
287
288                    let napi_attr = match method.kind {
289                        MethodKind::Constructor => quote!(#[napi(constructor)]),
290                        MethodKind::Static => quote!(#[napi]),
291                        MethodKind::Factory => quote!(#[napi(factory)]),
292                        MethodKind::Normal | MethodKind::Async | MethodKind::ToString => {
293                            quote!(#[napi])
294                        }
295                    };
296
297                    match method.kind {
298                        MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
299                            method_tokens.extend(quote! {
300                                #napi_attr
301                                pub fn #method_ident(
302                                    env: Env,
303                                    #( #arg_idents: #arg_types ),*
304                                ) -> napi::Result<#ret> {
305                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
306                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
307                                    )?, &bindy::NapiReturnContext(env))?)
308                                }
309                            });
310                        }
311                        MethodKind::Normal | MethodKind::ToString => {
312                            method_tokens.extend(quote! {
313                                #napi_attr
314                                pub fn #method_ident(
315                                    &self,
316                                    env: Env,
317                                    #( #arg_idents: #arg_types ),*
318                                ) -> napi::Result<#ret> {
319                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
320                                        &self.0,
321                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
322                                    )?, &bindy::NapiReturnContext(env))?)
323                                }
324                            });
325                        }
326                        MethodKind::Async => {
327                            method_tokens.extend(quote! {
328                                #napi_attr
329                                pub async fn #method_ident(
330                                    &self,
331                                    #( #arg_idents: #arg_types ),*
332                                ) -> napi::Result<#ret> {
333                                    Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#method_ident(
334                                        #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
335                                    ).await?, &bindy::NapiAsyncReturnContext)?)
336                                }
337                            });
338                        }
339                    }
340                }
341
342                let mut field_tokens = quote!();
343
344                for (name, ty) in &fields {
345                    let ident = Ident::new(name, Span::mixed_site());
346                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
347                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
348                    let get_ty =
349                        parse_str::<Type>(apply_mappings(ty, &return_mappings).as_str()).unwrap();
350                    let set_ty =
351                        parse_str::<Type>(apply_mappings(ty, &non_async_param_mappings).as_str())
352                            .unwrap();
353
354                    field_tokens.extend(quote! {
355                        #[napi(getter)]
356                        pub fn #get_ident(&self, env: Env) -> napi::Result<#get_ty> {
357                            Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#ident.clone(), &bindy::NapiReturnContext(env))?)
358                        }
359
360                        #[napi(setter)]
361                        pub fn #set_ident(&mut self, env: Env, value: #set_ty) -> napi::Result<()> {
362                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Napi>::into_rust(value, &bindy::NapiParamContext)?;
363                            Ok(())
364                        }
365                    });
366                }
367
368                if new {
369                    let arg_idents = fields
370                        .keys()
371                        .map(|k| Ident::new(k, Span::mixed_site()))
372                        .collect::<Vec<_>>();
373
374                    let arg_types = fields
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                    method_tokens.extend(quote! {
383                        #[napi(constructor)]
384                        pub fn new(
385                            env: Env,
386                            #( #arg_idents: #arg_types ),*
387                        ) -> napi::Result<Self> {
388                            Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#rust_struct_ident {
389                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)?),*
390                            }, &bindy::NapiReturnContext(env))?)
391                        }
392                    });
393                }
394
395                output.extend(quote! {
396                    #[napi_derive::napi]
397                    #[derive(Clone)]
398                    pub struct #bound_ident(#rust_struct_ident);
399
400                    #[napi_derive::napi]
401                    impl #bound_ident {
402                        #method_tokens
403                        #field_tokens
404                    }
405
406                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
407                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
408                            Ok(Self(value))
409                        }
410                    }
411
412                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
413                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
414                            Ok(self.0)
415                        }
416                    }
417                });
418            }
419            Binding::Enum { values } => {
420                let bound_ident = Ident::new(&name, Span::mixed_site());
421                let rust_ident = quote!( #entrypoint::#bound_ident );
422
423                let value_idents = values
424                    .iter()
425                    .map(|v| Ident::new(v, Span::mixed_site()))
426                    .collect::<Vec<_>>();
427
428                output.extend(quote! {
429                    #[napi_derive::napi]
430                    pub enum #bound_ident {
431                        #( #value_idents ),*
432                    }
433
434                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Napi> for #bound_ident {
435                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
436                            Ok(match value {
437                                #( #rust_ident::#value_idents => Self::#value_idents ),*
438                            })
439                        }
440                    }
441
442                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Napi> for #bound_ident {
443                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
444                            Ok(match self {
445                                #( Self::#value_idents => #rust_ident::#value_idents ),*
446                            })
447                        }
448                    }
449                });
450            }
451            Binding::Function { args, ret } => {
452                let bound_ident = Ident::new(&name, Span::mixed_site());
453                let ident = Ident::new(&name, Span::mixed_site());
454
455                let arg_idents = args
456                    .keys()
457                    .map(|k| Ident::new(k, Span::mixed_site()))
458                    .collect::<Vec<_>>();
459
460                let arg_types = args
461                    .values()
462                    .map(|v| {
463                        parse_str::<Type>(apply_mappings(v, &non_async_param_mappings).as_str())
464                            .unwrap()
465                    })
466                    .collect::<Vec<_>>();
467
468                let ret = parse_str::<Type>(
469                    apply_mappings(ret.as_deref().unwrap_or("()"), &return_mappings).as_str(),
470                )
471                .unwrap();
472
473                output.extend(quote! {
474                    #[napi_derive::napi]
475                    pub fn #bound_ident(
476                        env: Env,
477                        #( #arg_idents: #arg_types ),*
478                    ) -> napi::Result<#ret> {
479                        Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#entrypoint::#ident(
480                            #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
481                        )?, &bindy::NapiReturnContext(env))?)
482                    }
483                });
484            }
485        }
486    }
487
488    let clvm_types = bindy
489        .clvm_types
490        .iter()
491        .map(|s| Ident::new(s, Span::mixed_site()))
492        .collect::<Vec<_>>();
493
494    let mut value_index = 1;
495    let mut value_idents = Vec::new();
496    let mut remaining_clvm_types = clvm_types.clone();
497
498    while !remaining_clvm_types.is_empty() {
499        let value_ident = Ident::new(&format!("Value{value_index}"), Span::mixed_site());
500        value_index += 1;
501
502        let consumed = if remaining_clvm_types.len() <= 26 {
503            let either_ident = Ident::new(
504                &format!("Either{}", remaining_clvm_types.len()),
505                Span::mixed_site(),
506            );
507
508            output.extend(quote! {
509                type #value_ident<'a> = #either_ident< #( ClassInstance<'a, #remaining_clvm_types > ),* >;
510            });
511
512            mem::take(&mut remaining_clvm_types)
513        } else {
514            let either_ident = Ident::new("Either26", Span::mixed_site());
515            let next_value_ident = Ident::new(&format!("Value{}", value_index), Span::mixed_site());
516            let next_25 = remaining_clvm_types.drain(..25).collect::<Vec<_>>();
517
518            output.extend(quote! {
519                type #value_ident<'a> = #either_ident< #( ClassInstance<'a, #next_25 > ),*, #next_value_ident<'a> >;
520            });
521
522            next_25
523        };
524
525        value_idents.push((value_ident, consumed));
526    }
527
528    let mut extractor = proc_macro2::TokenStream::new();
529
530    for (i, (value_ident, consumed)) in value_idents.into_iter().rev().enumerate() {
531        let chain = (i > 0).then(|| quote!( #value_ident::Z(value) => #extractor, ));
532
533        let items = consumed
534            .iter()
535            .enumerate()
536            .map(|(i, ty)| {
537                let letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
538                    .chars()
539                    .nth(i)
540                    .unwrap()
541                    .to_string();
542                let letter = Ident::new(&letter, Span::mixed_site());
543                quote!( #value_ident::#letter(value) => ClvmType::#ty((*value).clone()) )
544            })
545            .collect::<Vec<_>>();
546
547        extractor = quote! {
548            match value {
549                #( #items, )*
550                #chain
551            }
552        };
553    }
554
555    output.extend(quote! {
556        enum ClvmType {
557            #( #clvm_types ( #clvm_types ), )*
558        }
559
560        fn extract_clvm_type(value: Value1) -> ClvmType {
561            #extractor
562        }
563    });
564
565    output.into()
566}
567
568#[proc_macro]
569pub fn bindy_wasm(input: TokenStream) -> TokenStream {
570    let input = syn::parse_macro_input!(input as LitStr).value();
571    let (bindy, bindings) = load_bindings(&input);
572
573    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
574
575    let mut base_mappings = bindy.wasm.clone();
576    let mut stubs = bindy.wasm_stubs.clone();
577    build_base_mappings(&bindy, &mut base_mappings, &mut stubs);
578
579    let mut param_mappings = base_mappings.clone();
580    let return_mappings = base_mappings;
581
582    for (name, binding) in &bindings {
583        if matches!(binding, Binding::Class { .. }) {
584            param_mappings.insert(
585                format!("Option<Vec<{name}>>"),
586                format!("&{name}OptionArrayType"),
587            );
588            param_mappings.insert(format!("Option<{name}>"), format!("&{name}OptionType"));
589            param_mappings.insert(format!("Vec<{name}>"), format!("&{name}ArrayType"));
590            param_mappings.insert(name.clone(), format!("&{name}"));
591
592            stubs.insert(
593                format!("Option<Vec<{name}>>"),
594                format!("{name}[] | undefined"),
595            );
596            stubs.insert(format!("Option<{name}>"), format!("{name} | undefined"));
597            stubs.insert(format!("Vec<{name}>"), format!("{name}[]"));
598        }
599    }
600
601    let mut output = quote!();
602    let mut js_types = quote!();
603
604    let mut classes = String::new();
605    let mut functions = String::new();
606
607    for (name, binding) in bindings {
608        match binding {
609            Binding::Class {
610                new,
611                remote,
612                methods,
613                fields,
614            } => {
615                let bound_ident = Ident::new(&name, Span::mixed_site());
616                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
617                let fully_qualified_ident = if remote {
618                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
619                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
620                } else {
621                    quote!( #rust_struct_ident )
622                };
623
624                let mut method_tokens = quote! {
625                    #[wasm_bindgen]
626                    pub fn clone(&self) -> Self {
627                        Clone::clone(self)
628                    }
629                };
630
631                let mut method_stubs = String::new();
632
633                let class_name = name.clone();
634
635                for (name, method) in methods {
636                    if !method.stub_only {
637                        let js_name = name.to_case(Case::Camel);
638                        let method_ident = Ident::new(&name, Span::mixed_site());
639
640                        let arg_attrs = method
641                            .args
642                            .keys()
643                            .map(|k| {
644                                let js_name = k.to_case(Case::Camel);
645                                quote!( #[wasm_bindgen(js_name = #js_name)] )
646                            })
647                            .collect::<Vec<_>>();
648
649                        let arg_idents = method
650                            .args
651                            .keys()
652                            .map(|k| Ident::new(k, Span::mixed_site()))
653                            .collect::<Vec<_>>();
654
655                        let arg_types = method
656                            .args
657                            .values()
658                            .map(|v| {
659                                parse_str::<Type>(apply_mappings(v, &param_mappings).as_str())
660                                    .unwrap()
661                            })
662                            .collect::<Vec<_>>();
663
664                        let ret = parse_str::<Type>(
665                            apply_mappings(
666                                method.ret.as_deref().unwrap_or(
667                                    if matches!(
668                                        method.kind,
669                                        MethodKind::Constructor | MethodKind::Factory
670                                    ) {
671                                        "Self"
672                                    } else {
673                                        "()"
674                                    },
675                                ),
676                                &return_mappings,
677                            )
678                            .as_str(),
679                        )
680                        .unwrap();
681
682                        let wasm_attr = match method.kind {
683                            MethodKind::Constructor => quote!(#[wasm_bindgen(constructor)]),
684                            _ => quote!(#[wasm_bindgen(js_name = #js_name)]),
685                        };
686
687                        match method.kind {
688                            MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
689                                method_tokens.extend(quote! {
690                                #wasm_attr
691                                pub fn #method_ident(
692                                    #( #arg_attrs #arg_idents: #arg_types ),*
693                                ) -> Result<#ret, wasm_bindgen::JsError> {
694                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
695                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
696                                    )?, &bindy::WasmContext)?)
697                                }
698                            });
699                            }
700                            MethodKind::Normal | MethodKind::ToString => {
701                                method_tokens.extend(quote! {
702                                #wasm_attr
703                                pub fn #method_ident(
704                                    &self,
705                                    #( #arg_attrs #arg_idents: #arg_types ),*
706                                ) -> Result<#ret, wasm_bindgen::JsError> {
707                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
708                                        &self.0,
709                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
710                                    )?, &bindy::WasmContext)?)
711                                }
712                            });
713                            }
714                            MethodKind::Async => {
715                                method_tokens.extend(quote! {
716                                #wasm_attr
717                                pub async fn #method_ident(
718                                    &self,
719                                    #( #arg_attrs #arg_idents: #arg_types ),*
720                                ) -> Result<#ret, wasm_bindgen::JsError> {
721                                    Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#method_ident(
722                                        #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
723                                    ).await?, &bindy::WasmContext)?)
724                                }
725                            });
726                            }
727                        }
728                    }
729
730                    let js_name = if matches!(method.kind, MethodKind::Constructor) {
731                        "constructor".to_string()
732                    } else {
733                        name.to_case(Case::Camel)
734                    };
735
736                    let arg_stubs = function_args(&method.args, &stubs, MappingFlavor::JavaScript);
737
738                    let mut ret_stub = apply_mappings_with_flavor(
739                        method.ret.as_deref().unwrap_or("()"),
740                        &stubs,
741                        MappingFlavor::JavaScript,
742                    );
743
744                    match method.kind {
745                        MethodKind::Async => ret_stub = format!("Promise<{ret_stub}>"),
746                        MethodKind::Factory => ret_stub = class_name.clone(),
747                        _ => {}
748                    }
749
750                    let prefix = match method.kind {
751                        MethodKind::Factory | MethodKind::Static => "static ",
752                        _ => "",
753                    };
754
755                    let ret_stub = if matches!(method.kind, MethodKind::Constructor) {
756                        "".to_string()
757                    } else {
758                        format!(": {ret_stub}")
759                    };
760
761                    method_stubs.push_str(&formatdoc! {"
762                        {prefix}{js_name}({arg_stubs}){ret_stub};
763                    "});
764                }
765
766                let mut field_tokens = quote!();
767                let mut field_stubs = String::new();
768
769                for (name, ty) in &fields {
770                    let js_name = name.to_case(Case::Camel);
771                    let ident = Ident::new(name, Span::mixed_site());
772                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
773                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
774                    let param_type =
775                        parse_str::<Type>(apply_mappings(ty, &param_mappings).as_str()).unwrap();
776                    let return_type =
777                        parse_str::<Type>(apply_mappings(ty, &return_mappings).as_str()).unwrap();
778
779                    field_tokens.extend(quote! {
780                        #[wasm_bindgen(getter, js_name = #js_name)]
781                        pub fn #get_ident(&self) -> Result<#return_type, wasm_bindgen::JsError> {
782                            Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#ident.clone(), &bindy::WasmContext)?)
783                        }
784
785                        #[wasm_bindgen(setter, js_name = #js_name)]
786                        pub fn #set_ident(&mut self, value: #param_type) -> Result<(), wasm_bindgen::JsError> {
787                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(value, &bindy::WasmContext)?;
788                            Ok(())
789                        }
790                    });
791
792                    let stub = apply_mappings_with_flavor(ty, &stubs, MappingFlavor::JavaScript);
793
794                    field_stubs.push_str(&formatdoc! {"
795                        {js_name}: {stub};
796                    "});
797                }
798
799                let mut constructor_stubs = String::new();
800
801                if new {
802                    let arg_attrs = fields
803                        .keys()
804                        .map(|k| {
805                            let js_name = k.to_case(Case::Camel);
806                            quote!( #[wasm_bindgen(js_name = #js_name)] )
807                        })
808                        .collect::<Vec<_>>();
809
810                    let arg_idents = fields
811                        .keys()
812                        .map(|k| Ident::new(k, Span::mixed_site()))
813                        .collect::<Vec<_>>();
814
815                    let arg_types = fields
816                        .values()
817                        .map(|v| {
818                            parse_str::<Type>(apply_mappings(v, &param_mappings).as_str()).unwrap()
819                        })
820                        .collect::<Vec<_>>();
821
822                    method_tokens.extend(quote! {
823                        #[wasm_bindgen(constructor)]
824                        pub fn new(
825                            #( #arg_attrs #arg_idents: #arg_types ),*
826                        ) -> Result<Self, wasm_bindgen::JsError> {
827                            Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#rust_struct_ident {
828                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)?),*
829                            }, &bindy::WasmContext)?)
830                        }
831                    });
832
833                    let arg_stubs = function_args(&fields, &stubs, MappingFlavor::JavaScript);
834
835                    constructor_stubs.push_str(&formatdoc! {"
836                        constructor({arg_stubs});
837                    "});
838                }
839
840                let option_type_ident =
841                    Ident::new(&format!("{name}OptionType"), Span::mixed_site());
842                let array_type_ident = Ident::new(&format!("{name}ArrayType"), Span::mixed_site());
843                let option_array_type_ident =
844                    Ident::new(&format!("{name}OptionArrayType"), Span::mixed_site());
845
846                js_types.extend(quote! {
847                    #[wasm_bindgen]
848                    pub type #option_type_ident;
849
850                    #[wasm_bindgen]
851                    pub type #array_type_ident;
852
853                    #[wasm_bindgen]
854                    pub type #option_array_type_ident;
855                });
856
857                output.extend(quote! {
858                    #[derive(TryFromJsValue)]
859                    #[wasm_bindgen(skip_typescript)]
860                    #[derive(Clone)]
861                    pub struct #bound_ident(#rust_struct_ident);
862
863                    #[wasm_bindgen]
864                    impl #bound_ident {
865                        #method_tokens
866                        #field_tokens
867                    }
868
869                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
870                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
871                            Ok(Self(value))
872                        }
873                    }
874
875                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
876                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
877                            Ok(self.0)
878                        }
879                    }
880
881                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Wasm> for &'_ #bound_ident {
882                        fn into_rust(self, context: &T) -> bindy::Result<#rust_struct_ident> {
883                            std::ops::Deref::deref(&self).clone().into_rust(context)
884                        }
885                    }
886
887                    impl<T> bindy::IntoRust<Option<#rust_struct_ident>, T, bindy::Wasm> for &'_ #option_type_ident {
888                        fn into_rust(self, context: &T) -> bindy::Result<Option<#rust_struct_ident>> {
889                            let typed_value = try_from_js_option::<#bound_ident>(self).map_err(bindy::Error::Custom)?;
890                            typed_value.into_rust(context)
891                        }
892                    }
893
894                    impl<T> bindy::IntoRust<Vec<#rust_struct_ident>, T, bindy::Wasm> for &'_ #array_type_ident {
895                        fn into_rust(self, context: &T) -> bindy::Result<Vec<#rust_struct_ident>> {
896                            let typed_value = try_from_js_array::<#bound_ident>(self).map_err(bindy::Error::Custom)?;
897                            typed_value.into_rust(context)
898                        }
899                    }
900
901                    impl<T> bindy::IntoRust<Option<Vec<#rust_struct_ident>>, T, bindy::Wasm> for &'_ #option_array_type_ident {
902                        fn into_rust(self, context: &T) -> bindy::Result<Option<Vec<#rust_struct_ident>>> {
903                            let typed_value = try_from_js_option_array::<#bound_ident>(self).map_err(bindy::Error::Custom)?;
904                            typed_value.into_rust(context)
905                        }
906                    }
907                });
908
909                let body_stubs = format!("{constructor_stubs}{field_stubs}{method_stubs}")
910                    .trim()
911                    .split("\n")
912                    .map(|s| format!("    {s}"))
913                    .collect::<Vec<_>>()
914                    .join("\n");
915
916                classes.push_str(&formatdoc! {"
917                    export class {name} {{
918                        free(): void;
919                        __getClassname(): string;
920                        clone(): {name};
921                    {body_stubs}
922                    }}
923                "});
924            }
925            Binding::Enum { values } => {
926                let bound_ident = Ident::new(&name, Span::mixed_site());
927                let rust_ident = quote!( #entrypoint::#bound_ident );
928
929                let value_idents = values
930                    .iter()
931                    .map(|v| Ident::new(v, Span::mixed_site()))
932                    .collect::<Vec<_>>();
933
934                output.extend(quote! {
935                    #[wasm_bindgen(skip_typescript)]
936                    #[derive(Clone)]
937                    pub enum #bound_ident {
938                        #( #value_idents ),*
939                    }
940
941                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
942                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
943                            Ok(match value {
944                                #( #rust_ident::#value_idents => Self::#value_idents ),*
945                            })
946                        }
947                    }
948
949                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
950                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
951                            Ok(match self {
952                                #( Self::#value_idents => #rust_ident::#value_idents ),*
953                            })
954                        }
955                    }
956                });
957
958                let body_stubs = values
959                    .iter()
960                    .enumerate()
961                    .map(|(i, v)| format!("    {v} = {i},"))
962                    .collect::<Vec<_>>()
963                    .join("\n");
964
965                classes.push_str(&formatdoc! {"
966                    export enum {name} {{
967                    {body_stubs}
968                    }}
969                "});
970            }
971            Binding::Function { args, ret } => {
972                let bound_ident = Ident::new(&name, Span::mixed_site());
973                let ident = Ident::new(&name, Span::mixed_site());
974
975                let js_name = name.to_case(Case::Camel);
976
977                let arg_attrs = args
978                    .keys()
979                    .map(|k| {
980                        let js_name = k.to_case(Case::Camel);
981                        quote!( #[wasm_bindgen(js_name = #js_name)] )
982                    })
983                    .collect::<Vec<_>>();
984
985                let arg_idents = args
986                    .keys()
987                    .map(|k| Ident::new(k, Span::mixed_site()))
988                    .collect::<Vec<_>>();
989
990                let arg_types = args
991                    .values()
992                    .map(|v| {
993                        parse_str::<Type>(apply_mappings(v, &param_mappings).as_str()).unwrap()
994                    })
995                    .collect::<Vec<_>>();
996
997                let ret_mapping = parse_str::<Type>(
998                    apply_mappings(ret.as_deref().unwrap_or("()"), &return_mappings).as_str(),
999                )
1000                .unwrap();
1001
1002                output.extend(quote! {
1003                    #[wasm_bindgen(skip_typescript, js_name = #js_name)]
1004                    pub fn #bound_ident(
1005                        #( #arg_attrs #arg_idents: #arg_types ),*
1006                    ) -> Result<#ret_mapping, wasm_bindgen::JsError> {
1007                        Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#entrypoint::#ident(
1008                            #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
1009                        )?, &bindy::WasmContext)?)
1010                    }
1011                });
1012
1013                let arg_stubs = function_args(&args, &stubs, MappingFlavor::JavaScript);
1014
1015                let ret_stub = apply_mappings_with_flavor(
1016                    ret.as_deref().unwrap_or("()"),
1017                    &stubs,
1018                    MappingFlavor::JavaScript,
1019                );
1020
1021                functions.push_str(&formatdoc! {"
1022                    export function {js_name}({arg_stubs}): {ret_stub};
1023                "});
1024            }
1025        }
1026    }
1027
1028    let clvm_type_values = [
1029        bindy.clvm_types.clone(),
1030        vec![
1031            "string | bigint | number | boolean | Uint8Array | null | undefined | ClvmType[]"
1032                .to_string(),
1033        ],
1034    ]
1035    .concat()
1036    .join(" | ");
1037    let clvm_type = format!("export type ClvmType = {clvm_type_values};");
1038
1039    let typescript = format!("\n{clvm_type}\n\n{functions}\n{classes}");
1040
1041    output.extend(quote! {
1042        #[wasm_bindgen]
1043        extern "C" {
1044            #js_types
1045        }
1046
1047        #[wasm_bindgen(typescript_custom_section)]
1048        const TS_APPEND_CONTENT: &'static str = #typescript;
1049    });
1050
1051    let clvm_types = bindy
1052        .clvm_types
1053        .iter()
1054        .map(|s| Ident::new(s, Span::mixed_site()))
1055        .collect::<Vec<_>>();
1056
1057    output.extend(quote! {
1058        enum ClvmType {
1059            #( #clvm_types ( #clvm_types ), )*
1060        }
1061
1062        fn try_from_js_any(js_val: &JsValue) -> Option<ClvmType> {
1063            #( if let Ok(value) = #clvm_types::try_from(js_val) {
1064                return Some(ClvmType::#clvm_types(value));
1065            } )*
1066
1067            None
1068        }
1069    });
1070
1071    output.into()
1072}
1073
1074#[proc_macro]
1075pub fn bindy_pyo3(input: TokenStream) -> TokenStream {
1076    let input = syn::parse_macro_input!(input as LitStr).value();
1077    let (bindy, bindings) = load_bindings(&input);
1078
1079    let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
1080
1081    let mut mappings = bindy.pyo3.clone();
1082    build_base_mappings(&bindy, &mut mappings, &mut IndexMap::new());
1083
1084    let mut output = quote!();
1085    let mut module = quote!();
1086
1087    for (name, binding) in bindings {
1088        let bound_ident = Ident::new(&name, Span::mixed_site());
1089
1090        match &binding {
1091            Binding::Class {
1092                new,
1093                remote,
1094                methods,
1095                fields,
1096            } => {
1097                let rust_struct_ident = quote!( #entrypoint::#bound_ident );
1098                let fully_qualified_ident = if *remote {
1099                    let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
1100                    quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
1101                } else {
1102                    quote!( #rust_struct_ident )
1103                };
1104
1105                let mut method_tokens = quote! {
1106                    pub fn clone(&self) -> Self {
1107                        Clone::clone(self)
1108                    }
1109                };
1110
1111                for (name, method) in methods {
1112                    if method.stub_only {
1113                        // TODO: Add stubs
1114                        continue;
1115                    }
1116
1117                    let method_ident = Ident::new(name, Span::mixed_site());
1118
1119                    let arg_idents = method
1120                        .args
1121                        .keys()
1122                        .map(|k| Ident::new(k, Span::mixed_site()))
1123                        .collect::<Vec<_>>();
1124
1125                    let arg_types = method
1126                        .args
1127                        .values()
1128                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
1129                        .collect::<Vec<_>>();
1130
1131                    let ret = parse_str::<Type>(
1132                        apply_mappings(
1133                            method.ret.as_deref().unwrap_or(
1134                                if matches!(
1135                                    method.kind,
1136                                    MethodKind::Constructor | MethodKind::Factory
1137                                ) {
1138                                    "Self"
1139                                } else {
1140                                    "()"
1141                                },
1142                            ),
1143                            &mappings,
1144                        )
1145                        .as_str(),
1146                    )
1147                    .unwrap();
1148
1149                    let mut pyo3_attr = match method.kind {
1150                        MethodKind::Constructor => quote!(#[new]),
1151                        MethodKind::Static => quote!(#[staticmethod]),
1152                        MethodKind::Factory => quote!(#[staticmethod]),
1153                        _ => quote!(),
1154                    };
1155
1156                    if !matches!(method.kind, MethodKind::ToString) {
1157                        pyo3_attr = quote! {
1158                            #pyo3_attr
1159                            #[pyo3(signature = (#(#arg_idents),*))]
1160                        };
1161                    }
1162
1163                    let remapped_method_ident = if matches!(method.kind, MethodKind::ToString) {
1164                        Ident::new("__str__", Span::mixed_site())
1165                    } else {
1166                        method_ident.clone()
1167                    };
1168
1169                    match method.kind {
1170                        MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
1171                            method_tokens.extend(quote! {
1172                                #pyo3_attr
1173                                pub fn #remapped_method_ident(
1174                                    #( #arg_idents: #arg_types ),*
1175                                ) -> pyo3::PyResult<#ret> {
1176                                    Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
1177                                        #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
1178                                    )?, &bindy::Pyo3Context)?)
1179                                }
1180                            });
1181                        }
1182                        MethodKind::Normal | MethodKind::ToString => {
1183                            method_tokens.extend(quote! {
1184                                #pyo3_attr
1185                                pub fn #remapped_method_ident(
1186                                    &self,
1187                                    #( #arg_idents: #arg_types ),*
1188                                ) -> pyo3::PyResult<#ret> {
1189                                    Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
1190                                        &self.0,
1191                                        #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
1192                                    )?, &bindy::Pyo3Context)?)
1193                                }
1194                            });
1195                        }
1196                        MethodKind::Async => {
1197                            method_tokens.extend(quote! {
1198                                #pyo3_attr
1199                                pub fn #remapped_method_ident<'a>(
1200                                    &self,
1201                                    py: Python<'a>,
1202                                    #( #arg_idents: #arg_types ),*
1203                                ) -> pyo3::PyResult<pyo3::Bound<'a, pyo3::PyAny>> {
1204                                    let clone_of_self = self.0.clone();
1205                                    #( let #arg_idents = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?; )*
1206
1207                                    pyo3_async_runtimes::tokio::future_into_py(py, async move {
1208                                        let result: pyo3::PyResult<#ret> = Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(clone_of_self.#method_ident(
1209                                            #( #arg_idents ),*
1210                                        ).await?, &bindy::Pyo3Context)?);
1211                                        result
1212                                    })
1213                                }
1214                            });
1215                        }
1216                    }
1217                }
1218
1219                let mut field_tokens = quote!();
1220
1221                for (name, ty) in fields {
1222                    let ident = Ident::new(name, Span::mixed_site());
1223                    let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
1224                    let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
1225                    let ty = parse_str::<Type>(apply_mappings(ty, &mappings).as_str()).unwrap();
1226
1227                    field_tokens.extend(quote! {
1228                        #[getter(#ident)]
1229                        pub fn #get_ident(&self) -> pyo3::PyResult<#ty> {
1230                            Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(self.0.#ident.clone(), &bindy::Pyo3Context)?)
1231                        }
1232
1233                        #[setter(#ident)]
1234                        pub fn #set_ident(&mut self, value: #ty) -> pyo3::PyResult<()> {
1235                            self.0.#ident = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(value, &bindy::Pyo3Context)?;
1236                            Ok(())
1237                        }
1238                    });
1239                }
1240
1241                if *new {
1242                    let arg_idents = fields
1243                        .keys()
1244                        .map(|k| Ident::new(k, Span::mixed_site()))
1245                        .collect::<Vec<_>>();
1246
1247                    let arg_types = fields
1248                        .values()
1249                        .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
1250                        .collect::<Vec<_>>();
1251
1252                    method_tokens.extend(quote! {
1253                        #[new]
1254                        #[pyo3(signature = (#(#arg_idents),*))]
1255                        pub fn new(
1256                            #( #arg_idents: #arg_types ),*
1257                        ) -> pyo3::PyResult<Self> {
1258                            Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#rust_struct_ident {
1259                                #(#arg_idents: bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?),*
1260                            }, &bindy::Pyo3Context)?)
1261                        }
1262                    });
1263                }
1264
1265                output.extend(quote! {
1266                    #[pyo3::pyclass]
1267                    #[derive(Clone)]
1268                    pub struct #bound_ident(#rust_struct_ident);
1269
1270                    #[pyo3::pymethods]
1271                    impl #bound_ident {
1272                        #method_tokens
1273                        #field_tokens
1274                    }
1275
1276                    impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
1277                        fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
1278                            Ok(Self(value))
1279                        }
1280                    }
1281
1282                    impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
1283                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
1284                            Ok(self.0)
1285                        }
1286                    }
1287                });
1288            }
1289            Binding::Enum { values } => {
1290                let bound_ident = Ident::new(&name, Span::mixed_site());
1291                let rust_ident = quote!( #entrypoint::#bound_ident );
1292
1293                let value_idents = values
1294                    .iter()
1295                    .map(|v| Ident::new(v, Span::mixed_site()))
1296                    .collect::<Vec<_>>();
1297
1298                output.extend(quote! {
1299                    #[pyo3::pyclass(eq, eq_int)]
1300                    #[derive(Clone, PartialEq, Eq)]
1301                    pub enum #bound_ident {
1302                        #( #value_idents ),*
1303                    }
1304
1305                    impl<T> bindy::FromRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
1306                        fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
1307                            Ok(match value {
1308                                #( #rust_ident::#value_idents => Self::#value_idents ),*
1309                            })
1310                        }
1311                    }
1312
1313                    impl<T> bindy::IntoRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
1314                        fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
1315                            Ok(match self {
1316                                #( Self::#value_idents => #rust_ident::#value_idents ),*
1317                            })
1318                        }
1319                    }
1320                });
1321            }
1322            Binding::Function { args, ret } => {
1323                let arg_idents = args
1324                    .keys()
1325                    .map(|k| Ident::new(k, Span::mixed_site()))
1326                    .collect::<Vec<_>>();
1327
1328                let arg_types = args
1329                    .values()
1330                    .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
1331                    .collect::<Vec<_>>();
1332
1333                let ret = parse_str::<Type>(
1334                    apply_mappings(ret.as_deref().unwrap_or("()"), &mappings).as_str(),
1335                )
1336                .unwrap();
1337
1338                output.extend(quote! {
1339                    #[pyo3::pyfunction]
1340                    #[pyo3(signature = (#(#arg_idents),*))]
1341                    pub fn #bound_ident(
1342                        #( #arg_idents: #arg_types ),*
1343                    ) -> pyo3::PyResult<#ret> {
1344                        Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#entrypoint::#bound_ident(
1345                            #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
1346                        )?, &bindy::Pyo3Context)?)
1347                    }
1348                });
1349            }
1350        }
1351
1352        match binding {
1353            Binding::Class { .. } => {
1354                module.extend(quote! {
1355                    m.add_class::<#bound_ident>()?;
1356                });
1357            }
1358            Binding::Enum { .. } => {
1359                module.extend(quote! {
1360                    m.add_class::<#bound_ident>()?;
1361                });
1362            }
1363            Binding::Function { .. } => {
1364                module.extend(quote! {
1365                    m.add_function(pyo3::wrap_pyfunction!(#bound_ident, m)?)?;
1366                });
1367            }
1368        }
1369    }
1370
1371    let pymodule = Ident::new(&bindy.pymodule, Span::mixed_site());
1372
1373    output.extend(quote! {
1374        #[pyo3::pymodule]
1375        fn #pymodule(m: &pyo3::Bound<'_, pyo3::prelude::PyModule>) -> pyo3::PyResult<()> {
1376            use pyo3::types::PyModuleMethods;
1377            #module
1378            Ok(())
1379        }
1380    });
1381
1382    let clvm_types = bindy
1383        .clvm_types
1384        .iter()
1385        .map(|s| Ident::new(s, Span::mixed_site()))
1386        .collect::<Vec<_>>();
1387
1388    output.extend(quote! {
1389        enum ClvmType {
1390            #( #clvm_types ( #clvm_types ), )*
1391        }
1392
1393        fn extract_clvm_type(value: &Bound<'_, PyAny>) -> Option<ClvmType> {
1394            #( if let Ok(value) = value.extract::<#clvm_types>() {
1395                return Some(ClvmType::#clvm_types(value));
1396            } )*
1397
1398            None
1399        }
1400    });
1401
1402    output.into()
1403}
1404
1405#[proc_macro]
1406pub fn bindy_pyo3_stubs(input: TokenStream) -> TokenStream {
1407    let input = syn::parse_macro_input!(input as LitStr).value();
1408    let (bindy, bindings) = load_bindings(&input);
1409
1410    let mut stubs = bindy.pyo3_stubs.clone();
1411    build_base_mappings(&bindy, &mut IndexMap::new(), &mut stubs);
1412
1413    let mut classes = String::new();
1414    let mut functions = String::new();
1415
1416    for (name, binding) in bindings {
1417        match binding {
1418            Binding::Class {
1419                new,
1420                methods,
1421                fields,
1422                ..
1423            } => {
1424                let mut method_stubs = String::new();
1425
1426                let class_name = name.clone();
1427
1428                for (name, method) in methods {
1429                    let name = if matches!(method.kind, MethodKind::Constructor) {
1430                        "__init__".to_string()
1431                    } else {
1432                        name
1433                    };
1434
1435                    let arg_stubs = function_args(&method.args, &stubs, MappingFlavor::Python);
1436
1437                    let mut ret_stub = apply_mappings_with_flavor(
1438                        method.ret.as_deref().unwrap_or("()"),
1439                        &stubs,
1440                        MappingFlavor::Python,
1441                    );
1442
1443                    match method.kind {
1444                        MethodKind::Async => ret_stub = format!("Awaitable[{ret_stub}]"),
1445                        MethodKind::Factory => ret_stub = class_name.clone(),
1446                        _ => {}
1447                    }
1448
1449                    let prefix = match method.kind {
1450                        MethodKind::Factory | MethodKind::Static => "@staticmethod\n",
1451                        MethodKind::Async => "async ",
1452                        _ => "",
1453                    };
1454
1455                    let self_arg =
1456                        if matches!(method.kind, MethodKind::Factory | MethodKind::Static) {
1457                            ""
1458                        } else if method.args.is_empty() {
1459                            "self"
1460                        } else {
1461                            "self, "
1462                        };
1463
1464                    method_stubs.push_str(&formatdoc! {"
1465                        {prefix}def {name}({self_arg}{arg_stubs}) -> {ret_stub}: ...
1466                    "});
1467                }
1468
1469                let mut field_stubs = String::new();
1470
1471                for (name, ty) in &fields {
1472                    let stub = apply_mappings_with_flavor(ty, &stubs, MappingFlavor::Python);
1473
1474                    field_stubs.push_str(&formatdoc! {"
1475                        {name}: {stub}
1476                    "});
1477                }
1478
1479                let mut constructor_stubs = String::new();
1480
1481                if new {
1482                    let arg_stubs = function_args(&fields, &stubs, MappingFlavor::Python);
1483
1484                    constructor_stubs.push_str(&formatdoc! {"
1485                        def __init__(self, {arg_stubs}) -> None: ...
1486                    "});
1487                }
1488
1489                let body_stubs = format!("{constructor_stubs}{field_stubs}{method_stubs}")
1490                    .trim()
1491                    .split("\n")
1492                    .map(|s| format!("    {s}"))
1493                    .collect::<Vec<_>>()
1494                    .join("\n");
1495
1496                classes.push_str(&formatdoc! {"
1497                    class {name}:
1498                        def clone(self) -> {name}: ...
1499                    {body_stubs}
1500                "});
1501            }
1502            Binding::Enum { values } => {
1503                let body_stubs = values
1504                    .iter()
1505                    .enumerate()
1506                    .map(|(i, v)| format!("    {v} = {i}"))
1507                    .collect::<Vec<_>>()
1508                    .join("\n");
1509
1510                classes.push_str(&formatdoc! {"
1511                    class {name}(IntEnum):
1512                    {body_stubs}
1513                "});
1514            }
1515            Binding::Function { args, ret } => {
1516                let arg_stubs = function_args(&args, &stubs, MappingFlavor::Python);
1517
1518                let ret_stub = apply_mappings_with_flavor(
1519                    ret.as_deref().unwrap_or("()"),
1520                    &stubs,
1521                    MappingFlavor::Python,
1522                );
1523
1524                functions.push_str(&formatdoc! {"
1525                    def {name}({arg_stubs}) -> {ret_stub}: ...
1526                "});
1527            }
1528        }
1529    }
1530
1531    let clvm_type_values = [
1532        bindy.clvm_types.clone(),
1533        vec!["str, int, bool, bytes, None, List['ClvmType']".to_string()],
1534    ]
1535    .concat()
1536    .join(", ");
1537    let clvm_type = format!("ClvmType = Union[{clvm_type_values}]");
1538
1539    let stubs = format!(
1540        "from typing import List, Optional, Union, Awaitable\nfrom enum import IntEnum\n\n{clvm_type}\n\n{functions}\n{classes}"
1541    );
1542
1543    quote!(#stubs).into()
1544}
1545
1546#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1547enum MappingFlavor {
1548    Rust,
1549    JavaScript,
1550    Python,
1551}
1552
1553fn apply_mappings(ty: &str, mappings: &IndexMap<String, String>) -> String {
1554    apply_mappings_with_flavor(ty, mappings, MappingFlavor::Rust)
1555}
1556
1557fn apply_mappings_with_flavor(
1558    ty: &str,
1559    mappings: &IndexMap<String, String>,
1560    flavor: MappingFlavor,
1561) -> String {
1562    // First check if the entire type has a direct mapping
1563    if let Some(mapped) = mappings.get(ty) {
1564        return mapped.clone();
1565    }
1566
1567    // Check if this is a generic type by looking for < and >
1568    if let (Some(start), Some(end)) = (ty.find('<'), ty.rfind('>')) {
1569        let base_type = &ty[..start];
1570        let generic_part = &ty[start + 1..end];
1571
1572        // Split generic parameters by comma and trim whitespace
1573        let generic_params: Vec<&str> = generic_part.split(',').map(|s| s.trim()).collect();
1574
1575        // Recursively apply mappings to each generic parameter
1576        let mapped_params: Vec<String> = generic_params
1577            .into_iter()
1578            .map(|param| apply_mappings_with_flavor(param, mappings, flavor))
1579            .collect();
1580
1581        // Check if the base type needs mapping
1582        let mapped_base = mappings
1583            .get(base_type)
1584            .map(|s| s.as_str())
1585            .unwrap_or(base_type);
1586
1587        // Reconstruct the type with mapped components
1588        match (flavor, mapped_base) {
1589            (MappingFlavor::Rust, _) => {
1590                format!("{}<{}>", mapped_base, mapped_params.join(", "))
1591            }
1592            (MappingFlavor::JavaScript, "Option") => {
1593                format!("{} | undefined", mapped_params[0])
1594            }
1595            (MappingFlavor::JavaScript, "Vec") => {
1596                format!("{}[]", mapped_params[0])
1597            }
1598            (MappingFlavor::Python, "Option") => {
1599                format!("Optional[{}]", mapped_params[0])
1600            }
1601            (MappingFlavor::Python, "Vec") => {
1602                format!("List[{}]", mapped_params[0])
1603            }
1604            _ => panic!("Unsupported mapping with flavor {flavor:?} for type {ty}"),
1605        }
1606    } else {
1607        // No generics, return original if no mapping exists
1608        ty.to_string()
1609    }
1610}
1611
1612fn function_args(
1613    args: &IndexMap<String, String>,
1614    stubs: &IndexMap<String, String>,
1615    mapping_flavor: MappingFlavor,
1616) -> String {
1617    let mut has_non_optional = false;
1618    let mut results = Vec::new();
1619
1620    for (name, ty) in args.iter().rev() {
1621        let is_optional = ty.starts_with("Option<");
1622        let has_default = is_optional && !has_non_optional;
1623        let ty = apply_mappings_with_flavor(ty, stubs, mapping_flavor);
1624
1625        results.push(format!(
1626            "{}{}: {}{}",
1627            name.to_case(Case::Camel),
1628            if has_default && matches!(mapping_flavor, MappingFlavor::JavaScript) {
1629                "?"
1630            } else {
1631                ""
1632            },
1633            ty,
1634            if has_default && matches!(mapping_flavor, MappingFlavor::Python) {
1635                " = None"
1636            } else {
1637                ""
1638            }
1639        ));
1640
1641        if !is_optional {
1642            has_non_optional = true;
1643        }
1644    }
1645
1646    results.reverse();
1647    results.join(", ")
1648}