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 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, ¶m_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, ¶m_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, ¶m_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, ¶m_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 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 if let Some(mapped) = mappings.get(ty) {
1629 return mapped.clone();
1630 }
1631
1632 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 let generic_params: Vec<&str> = generic_part.split(',').map(str::trim).collect();
1639
1640 let mapped_params: Vec<String> = generic_params
1642 .into_iter()
1643 .map(|param| apply_mappings_with_flavor(param, mappings, flavor))
1644 .collect();
1645
1646 let mapped_base = mappings.get(base_type).map_or(base_type, String::as_str);
1648
1649 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 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}