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 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, ¶m_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, ¶m_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, ¶m_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, ¶m_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 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 if let Some(mapped) = mappings.get(ty) {
1638 return mapped.clone();
1639 }
1640
1641 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 let generic_params: Vec<&str> = generic_part.split(',').map(str::trim).collect();
1648
1649 let mapped_params: Vec<String> = generic_params
1651 .into_iter()
1652 .map(|param| apply_mappings_with_flavor(param, mappings, flavor))
1653 .collect();
1654
1655 let mapped_base = mappings.get(base_type).map_or(base_type, String::as_str);
1657
1658 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 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}