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