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