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