1use std::{fs, path::Path};
2
3use chia_sdk_bindings::CONSTANTS;
4use convert_case::{Case, Casing};
5use indexmap::IndexMap;
6use proc_macro::TokenStream;
7use proc_macro2::Span;
8use quote::quote;
9use serde::{Deserialize, Serialize};
10use syn::{parse_str, Ident, LitStr, Type};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13struct Bindy {
14 entrypoint: String,
15 pymodule: String,
16 #[serde(default)]
17 type_groups: IndexMap<String, Vec<String>>,
18 #[serde(default)]
19 shared: IndexMap<String, String>,
20 #[serde(default)]
21 napi: IndexMap<String, String>,
22 #[serde(default)]
23 wasm: IndexMap<String, String>,
24 #[serde(default)]
25 pyo3: IndexMap<String, String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "type", rename_all = "snake_case")]
30enum Binding {
31 Class {
32 #[serde(default)]
33 new: bool,
34 #[serde(default)]
35 fields: IndexMap<String, String>,
36 #[serde(default)]
37 methods: IndexMap<String, Method>,
38 #[serde(default)]
39 remote: bool,
40 },
41 Enum {
42 values: Vec<String>,
43 },
44 Function {
45 #[serde(default)]
46 args: IndexMap<String, String>,
47 #[serde(rename = "return")]
48 ret: Option<String>,
49 },
50}
51
52#[derive(Debug, Default, Clone, Serialize, Deserialize)]
53#[serde(default)]
54struct Method {
55 #[serde(rename = "type")]
56 kind: MethodKind,
57 args: IndexMap<String, String>,
58 #[serde(rename = "return")]
59 ret: Option<String>,
60}
61
62#[derive(Debug, Default, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64enum MethodKind {
65 #[default]
66 Normal,
67 Async,
68 ToString,
69 Static,
70 Factory,
71 Constructor,
72}
73
74fn load_bindings(path: &str) -> (Bindy, IndexMap<String, Binding>) {
75 let source = fs::read_to_string(path).unwrap();
76 let bindy: Bindy = serde_json::from_str(&source).unwrap();
77
78 let mut bindings = IndexMap::new();
79
80 let mut dir: Vec<_> = fs::read_dir(Path::new(path).parent().unwrap().join("bindings"))
81 .unwrap()
82 .map(|p| p.unwrap())
83 .collect();
84
85 dir.sort_by_key(|p| p.path().file_name().unwrap().to_str().unwrap().to_string());
86
87 for path in dir {
88 if path.path().extension().unwrap() == "json" {
89 let source = fs::read_to_string(path.path()).unwrap();
90 let contents: IndexMap<String, Binding> = serde_json::from_str(&source).unwrap();
91 bindings.extend(contents);
92 }
93 }
94
95 if let Binding::Class { methods, .. } =
96 &mut bindings.get_mut("Constants").expect("Constants not found")
97 {
98 for &name in CONSTANTS {
99 methods.insert(
100 name.to_string(),
101 Method {
102 kind: MethodKind::Static,
103 args: IndexMap::new(),
104 ret: Some("SerializedProgram".to_string()),
105 },
106 );
107
108 methods.insert(
109 format!("{name}_hash"),
110 Method {
111 kind: MethodKind::Static,
112 args: IndexMap::new(),
113 ret: Some("TreeHash".to_string()),
114 },
115 );
116 }
117 }
118
119 if let Binding::Class { methods, .. } = &mut bindings.get_mut("Clvm").expect("Clvm not found") {
120 for &name in CONSTANTS {
121 methods.insert(
122 name.to_string(),
123 Method {
124 kind: MethodKind::Normal,
125 args: IndexMap::new(),
126 ret: Some("Program".to_string()),
127 },
128 );
129 }
130 }
131
132 (bindy, bindings)
133}
134
135fn build_base_mappings(bindy: &Bindy, mappings: &mut IndexMap<String, String>) {
136 for (name, value) in &bindy.shared {
137 if !mappings.contains_key(name) {
138 mappings.insert(name.clone(), value.clone());
139 }
140 }
141
142 for (name, group) in &bindy.type_groups {
143 if let Some(value) = mappings.shift_remove(name) {
144 for ty in group {
145 mappings.insert(ty.clone(), value.clone());
146 }
147 }
148 }
149}
150
151#[proc_macro]
152pub fn bindy_napi(input: TokenStream) -> TokenStream {
153 let input = syn::parse_macro_input!(input as LitStr).value();
154 let (bindy, bindings) = load_bindings(&input);
155
156 let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
157
158 let mut base_mappings = bindy.napi.clone();
159 build_base_mappings(&bindy, &mut base_mappings);
160
161 let mut non_async_param_mappings = base_mappings.clone();
162 let mut async_param_mappings = base_mappings.clone();
163 let mut return_mappings = base_mappings;
164
165 for (name, binding) in &bindings {
166 if matches!(binding, Binding::Class { .. }) {
167 non_async_param_mappings.insert(
168 name.clone(),
169 format!("napi::bindgen_prelude::ClassInstance<{name}>"),
170 );
171 async_param_mappings.insert(
172 name.clone(),
173 format!("napi::bindgen_prelude::Reference<{name}>"),
174 );
175 }
176 }
177
178 for ty in return_mappings.values_mut() {
181 if ty.as_str() == "napi::bindgen_prelude::Uint8Array" {
182 *ty = "napi::bindgen_prelude::Buffer".to_string();
183 }
184 }
185
186 let mut output = quote!();
187
188 for (name, binding) in bindings {
189 match binding {
190 Binding::Class {
191 new,
192 remote,
193 methods,
194 fields,
195 } => {
196 let bound_ident = Ident::new(&name, Span::mixed_site());
197 let rust_struct_ident = quote!( #entrypoint::#bound_ident );
198 let fully_qualified_ident = if remote {
199 let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
200 quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
201 } else {
202 quote!( #rust_struct_ident )
203 };
204
205 let mut method_tokens = quote! {
206 #[napi]
207 pub fn clone(&self) -> Self {
208 Clone::clone(self)
209 }
210 };
211
212 for (name, method) in methods {
213 let method_ident = Ident::new(&name, Span::mixed_site());
214
215 let param_mappings = if matches!(method.kind, MethodKind::Async) {
216 &async_param_mappings
217 } else {
218 &non_async_param_mappings
219 };
220
221 let arg_idents = method
222 .args
223 .keys()
224 .map(|k| Ident::new(k, Span::mixed_site()))
225 .collect::<Vec<_>>();
226
227 let arg_types = method
228 .args
229 .values()
230 .map(|v| {
231 parse_str::<Type>(apply_mappings(v, param_mappings).as_str()).unwrap()
232 })
233 .collect::<Vec<_>>();
234
235 let ret = parse_str::<Type>(
236 apply_mappings(
237 method.ret.as_deref().unwrap_or(
238 if matches!(
239 method.kind,
240 MethodKind::Constructor | MethodKind::Factory
241 ) {
242 "Self"
243 } else {
244 "()"
245 },
246 ),
247 &return_mappings,
248 )
249 .as_str(),
250 )
251 .unwrap();
252
253 let napi_attr = match method.kind {
254 MethodKind::Constructor => quote!(#[napi(constructor)]),
255 MethodKind::Static => quote!(#[napi]),
256 MethodKind::Factory => quote!(#[napi(factory)]),
257 MethodKind::Normal | MethodKind::Async | MethodKind::ToString => {
258 quote!(#[napi])
259 }
260 };
261
262 match method.kind {
263 MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
264 method_tokens.extend(quote! {
265 #napi_attr
266 pub fn #method_ident(
267 env: Env,
268 #( #arg_idents: #arg_types ),*
269 ) -> napi::Result<#ret> {
270 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
271 #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
272 )?, &bindy::NapiReturnContext(env))?)
273 }
274 });
275 }
276 MethodKind::Normal | MethodKind::ToString => {
277 method_tokens.extend(quote! {
278 #napi_attr
279 pub fn #method_ident(
280 &self,
281 env: Env,
282 #( #arg_idents: #arg_types ),*
283 ) -> napi::Result<#ret> {
284 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#fully_qualified_ident::#method_ident(
285 &self.0,
286 #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
287 )?, &bindy::NapiReturnContext(env))?)
288 }
289 });
290 }
291 MethodKind::Async => {
292 method_tokens.extend(quote! {
293 #napi_attr
294 pub async fn #method_ident(
295 &self,
296 #( #arg_idents: #arg_types ),*
297 ) -> napi::Result<#ret> {
298 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#method_ident(
299 #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
300 ).await?, &bindy::NapiAsyncReturnContext)?)
301 }
302 });
303 }
304 }
305 }
306
307 let mut field_tokens = quote!();
308
309 for (name, ty) in &fields {
310 let ident = Ident::new(name, Span::mixed_site());
311 let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
312 let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
313 let get_ty =
314 parse_str::<Type>(apply_mappings(ty, &return_mappings).as_str()).unwrap();
315 let set_ty =
316 parse_str::<Type>(apply_mappings(ty, &non_async_param_mappings).as_str())
317 .unwrap();
318
319 field_tokens.extend(quote! {
320 #[napi(getter)]
321 pub fn #get_ident(&self, env: Env) -> napi::Result<#get_ty> {
322 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(self.0.#ident.clone(), &bindy::NapiReturnContext(env))?)
323 }
324
325 #[napi(setter)]
326 pub fn #set_ident(&mut self, env: Env, value: #set_ty) -> napi::Result<()> {
327 self.0.#ident = bindy::IntoRust::<_, _, bindy::Napi>::into_rust(value, &bindy::NapiParamContext)?;
328 Ok(())
329 }
330 });
331 }
332
333 if new {
334 let arg_idents = fields
335 .keys()
336 .map(|k| Ident::new(k, Span::mixed_site()))
337 .collect::<Vec<_>>();
338
339 let arg_types = fields
340 .values()
341 .map(|v| {
342 parse_str::<Type>(apply_mappings(v, &non_async_param_mappings).as_str())
343 .unwrap()
344 })
345 .collect::<Vec<_>>();
346
347 method_tokens.extend(quote! {
348 #[napi(constructor)]
349 pub fn new(
350 env: Env,
351 #( #arg_idents: #arg_types ),*
352 ) -> napi::Result<Self> {
353 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#rust_struct_ident {
354 #(#arg_idents: bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)?),*
355 }, &bindy::NapiReturnContext(env))?)
356 }
357 });
358 }
359
360 output.extend(quote! {
361 #[napi_derive::napi]
362 #[derive(Clone)]
363 pub struct #bound_ident(#rust_struct_ident);
364
365 #[napi_derive::napi]
366 impl #bound_ident {
367 #method_tokens
368 #field_tokens
369 }
370
371 impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
372 fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
373 Ok(Self(value))
374 }
375 }
376
377 impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Napi> for #bound_ident {
378 fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
379 Ok(self.0)
380 }
381 }
382 });
383 }
384 Binding::Enum { values } => {
385 let bound_ident = Ident::new(&name, Span::mixed_site());
386 let rust_ident = quote!( #entrypoint::#bound_ident );
387
388 let value_idents = values
389 .iter()
390 .map(|v| Ident::new(v, Span::mixed_site()))
391 .collect::<Vec<_>>();
392
393 output.extend(quote! {
394 #[napi_derive::napi]
395 pub enum #bound_ident {
396 #( #value_idents ),*
397 }
398
399 impl<T> bindy::FromRust<#rust_ident, T, bindy::Napi> for #bound_ident {
400 fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
401 Ok(match value {
402 #( #rust_ident::#value_idents => Self::#value_idents ),*
403 })
404 }
405 }
406
407 impl<T> bindy::IntoRust<#rust_ident, T, bindy::Napi> for #bound_ident {
408 fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
409 Ok(match self {
410 #( Self::#value_idents => #rust_ident::#value_idents ),*
411 })
412 }
413 }
414 });
415 }
416 Binding::Function { args, ret } => {
417 let bound_ident = Ident::new(&name, Span::mixed_site());
418 let ident = Ident::new(&name, Span::mixed_site());
419
420 let arg_idents = args
421 .keys()
422 .map(|k| Ident::new(k, Span::mixed_site()))
423 .collect::<Vec<_>>();
424
425 let arg_types = args
426 .values()
427 .map(|v| {
428 parse_str::<Type>(apply_mappings(v, &non_async_param_mappings).as_str())
429 .unwrap()
430 })
431 .collect::<Vec<_>>();
432
433 let ret = parse_str::<Type>(
434 apply_mappings(ret.as_deref().unwrap_or("()"), &return_mappings).as_str(),
435 )
436 .unwrap();
437
438 output.extend(quote! {
439 #[napi_derive::napi]
440 pub fn #bound_ident(
441 env: Env,
442 #( #arg_idents: #arg_types ),*
443 ) -> napi::Result<#ret> {
444 Ok(bindy::FromRust::<_, _, bindy::Napi>::from_rust(#entrypoint::#ident(
445 #( bindy::IntoRust::<_, _, bindy::Napi>::into_rust(#arg_idents, &bindy::NapiParamContext)? ),*
446 )?, &bindy::NapiReturnContext(env))?)
447 }
448 });
449 }
450 }
451 }
452
453 output.into()
454}
455
456#[proc_macro]
457pub fn bindy_wasm(input: TokenStream) -> TokenStream {
458 let input = syn::parse_macro_input!(input as LitStr).value();
459 let (bindy, bindings) = load_bindings(&input);
460
461 let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
462
463 let mut mappings = bindy.wasm.clone();
464 build_base_mappings(&bindy, &mut mappings);
465
466 let mut output = quote!();
467
468 for (name, binding) in bindings {
469 match binding {
470 Binding::Class {
471 new,
472 remote,
473 methods,
474 fields,
475 } => {
476 let bound_ident = Ident::new(&name, Span::mixed_site());
477 let rust_struct_ident = quote!( #entrypoint::#bound_ident );
478 let fully_qualified_ident = if remote {
479 let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
480 quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
481 } else {
482 quote!( #rust_struct_ident )
483 };
484
485 let mut method_tokens = quote! {
486 #[wasm_bindgen]
487 pub fn clone(&self) -> Self {
488 Clone::clone(self)
489 }
490 };
491
492 for (name, method) in methods {
493 let js_name = name.to_case(Case::Camel);
494 let method_ident = Ident::new(&name, Span::mixed_site());
495
496 let arg_attrs = method
497 .args
498 .keys()
499 .map(|k| {
500 let js_name = k.to_case(Case::Camel);
501 quote!( #[wasm_bindgen(js_name = #js_name)] )
502 })
503 .collect::<Vec<_>>();
504
505 let arg_idents = method
506 .args
507 .keys()
508 .map(|k| Ident::new(k, Span::mixed_site()))
509 .collect::<Vec<_>>();
510
511 let arg_types = method
512 .args
513 .values()
514 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
515 .collect::<Vec<_>>();
516
517 let ret = parse_str::<Type>(
518 apply_mappings(
519 method.ret.as_deref().unwrap_or(
520 if matches!(
521 method.kind,
522 MethodKind::Constructor | MethodKind::Factory
523 ) {
524 "Self"
525 } else {
526 "()"
527 },
528 ),
529 &mappings,
530 )
531 .as_str(),
532 )
533 .unwrap();
534
535 let wasm_attr = match method.kind {
536 MethodKind::Constructor => quote!(#[wasm_bindgen(constructor)]),
537 _ => quote!(#[wasm_bindgen(js_name = #js_name)]),
538 };
539
540 match method.kind {
541 MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
542 method_tokens.extend(quote! {
543 #wasm_attr
544 pub fn #method_ident(
545 #( #arg_attrs #arg_idents: #arg_types ),*
546 ) -> Result<#ret, wasm_bindgen::JsError> {
547 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
548 #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
549 )?, &bindy::WasmContext)?)
550 }
551 });
552 }
553 MethodKind::Normal | MethodKind::ToString => {
554 method_tokens.extend(quote! {
555 #wasm_attr
556 pub fn #method_ident(
557 &self,
558 #( #arg_attrs #arg_idents: #arg_types ),*
559 ) -> Result<#ret, wasm_bindgen::JsError> {
560 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#fully_qualified_ident::#method_ident(
561 &self.0,
562 #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
563 )?, &bindy::WasmContext)?)
564 }
565 });
566 }
567 MethodKind::Async => {
568 method_tokens.extend(quote! {
569 #wasm_attr
570 pub async fn #method_ident(
571 &self,
572 #( #arg_attrs #arg_idents: #arg_types ),*
573 ) -> Result<#ret, wasm_bindgen::JsError> {
574 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#method_ident(
575 #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
576 ).await?, &bindy::WasmContext)?)
577 }
578 });
579 }
580 }
581 }
582
583 let mut field_tokens = quote!();
584
585 for (name, ty) in &fields {
586 let js_name = name.to_case(Case::Camel);
587 let ident = Ident::new(name, Span::mixed_site());
588 let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
589 let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
590 let ty = parse_str::<Type>(apply_mappings(ty, &mappings).as_str()).unwrap();
591
592 field_tokens.extend(quote! {
593 #[wasm_bindgen(getter, js_name = #js_name)]
594 pub fn #get_ident(&self) -> Result<#ty, wasm_bindgen::JsError> {
595 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(self.0.#ident.clone(), &bindy::WasmContext)?)
596 }
597
598 #[wasm_bindgen(setter, js_name = #js_name)]
599 pub fn #set_ident(&mut self, value: #ty) -> Result<(), wasm_bindgen::JsError> {
600 self.0.#ident = bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(value, &bindy::WasmContext)?;
601 Ok(())
602 }
603 });
604 }
605
606 if new {
607 let arg_attrs = fields
608 .keys()
609 .map(|k| {
610 let js_name = k.to_case(Case::Camel);
611 quote!( #[wasm_bindgen(js_name = #js_name)] )
612 })
613 .collect::<Vec<_>>();
614
615 let arg_idents = fields
616 .keys()
617 .map(|k| Ident::new(k, Span::mixed_site()))
618 .collect::<Vec<_>>();
619
620 let arg_types = fields
621 .values()
622 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
623 .collect::<Vec<_>>();
624
625 method_tokens.extend(quote! {
626 #[wasm_bindgen(constructor)]
627 pub fn new(
628 #( #arg_attrs #arg_idents: #arg_types ),*
629 ) -> Result<Self, wasm_bindgen::JsError> {
630 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#rust_struct_ident {
631 #(#arg_idents: bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)?),*
632 }, &bindy::WasmContext)?)
633 }
634 });
635 }
636
637 output.extend(quote! {
638 #[wasm_bindgen::prelude::wasm_bindgen]
639 #[derive(Clone)]
640 pub struct #bound_ident(#rust_struct_ident);
641
642 #[wasm_bindgen::prelude::wasm_bindgen]
643 impl #bound_ident {
644 #method_tokens
645 #field_tokens
646 }
647
648 impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
649 fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
650 Ok(Self(value))
651 }
652 }
653
654 impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Wasm> for #bound_ident {
655 fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
656 Ok(self.0)
657 }
658 }
659 });
660 }
661 Binding::Enum { values } => {
662 let bound_ident = Ident::new(&name, Span::mixed_site());
663 let rust_ident = quote!( #entrypoint::#bound_ident );
664
665 let value_idents = values
666 .iter()
667 .map(|v| Ident::new(v, Span::mixed_site()))
668 .collect::<Vec<_>>();
669
670 output.extend(quote! {
671 #[wasm_bindgen::prelude::wasm_bindgen]
672 #[derive(Clone)]
673 pub enum #bound_ident {
674 #( #value_idents ),*
675 }
676
677 impl<T> bindy::FromRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
678 fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
679 Ok(match value {
680 #( #rust_ident::#value_idents => Self::#value_idents ),*
681 })
682 }
683 }
684
685 impl<T> bindy::IntoRust<#rust_ident, T, bindy::Wasm> for #bound_ident {
686 fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
687 Ok(match self {
688 #( Self::#value_idents => #rust_ident::#value_idents ),*
689 })
690 }
691 }
692 });
693 }
694 Binding::Function { args, ret } => {
695 let bound_ident = Ident::new(&name, Span::mixed_site());
696 let ident = Ident::new(&name, Span::mixed_site());
697
698 let js_name = name.to_case(Case::Camel);
699
700 let arg_attrs = args
701 .keys()
702 .map(|k| {
703 let js_name = k.to_case(Case::Camel);
704 quote!( #[wasm_bindgen(js_name = #js_name)] )
705 })
706 .collect::<Vec<_>>();
707
708 let arg_idents = args
709 .keys()
710 .map(|k| Ident::new(k, Span::mixed_site()))
711 .collect::<Vec<_>>();
712
713 let arg_types = args
714 .values()
715 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
716 .collect::<Vec<_>>();
717
718 let ret = parse_str::<Type>(
719 apply_mappings(ret.as_deref().unwrap_or("()"), &mappings).as_str(),
720 )
721 .unwrap();
722
723 output.extend(quote! {
724 #[wasm_bindgen::prelude::wasm_bindgen(js_name = #js_name)]
725 pub fn #bound_ident(
726 #( #arg_attrs #arg_idents: #arg_types ),*
727 ) -> Result<#ret, wasm_bindgen::JsError> {
728 Ok(bindy::FromRust::<_, _, bindy::Wasm>::from_rust(#entrypoint::#ident(
729 #( bindy::IntoRust::<_, _, bindy::Wasm>::into_rust(#arg_idents, &bindy::WasmContext)? ),*
730 )?, &bindy::WasmContext)?)
731 }
732 });
733 }
734 }
735 }
736
737 output.into()
738}
739
740#[proc_macro]
741pub fn bindy_pyo3(input: TokenStream) -> TokenStream {
742 let input = syn::parse_macro_input!(input as LitStr).value();
743 let (bindy, bindings) = load_bindings(&input);
744
745 let entrypoint = Ident::new(&bindy.entrypoint, Span::mixed_site());
746
747 let mut mappings = bindy.pyo3.clone();
748 build_base_mappings(&bindy, &mut mappings);
749
750 let mut output = quote!();
751 let mut module = quote!();
752
753 for (name, binding) in bindings {
754 let bound_ident = Ident::new(&name, Span::mixed_site());
755
756 match &binding {
757 Binding::Class {
758 new,
759 remote,
760 methods,
761 fields,
762 } => {
763 let rust_struct_ident = quote!( #entrypoint::#bound_ident );
764 let fully_qualified_ident = if *remote {
765 let ext_ident = Ident::new(&format!("{name}Ext"), Span::mixed_site());
766 quote!( <#rust_struct_ident as #entrypoint::#ext_ident> )
767 } else {
768 quote!( #rust_struct_ident )
769 };
770
771 let mut method_tokens = quote! {
772 pub fn clone(&self) -> Self {
773 Clone::clone(self)
774 }
775 };
776
777 for (name, method) in methods {
778 let method_ident = Ident::new(name, Span::mixed_site());
779
780 let arg_idents = method
781 .args
782 .keys()
783 .map(|k| Ident::new(k, Span::mixed_site()))
784 .collect::<Vec<_>>();
785
786 let arg_types = method
787 .args
788 .values()
789 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
790 .collect::<Vec<_>>();
791
792 let ret = parse_str::<Type>(
793 apply_mappings(
794 method.ret.as_deref().unwrap_or(
795 if matches!(
796 method.kind,
797 MethodKind::Constructor | MethodKind::Factory
798 ) {
799 "Self"
800 } else {
801 "()"
802 },
803 ),
804 &mappings,
805 )
806 .as_str(),
807 )
808 .unwrap();
809
810 let mut pyo3_attr = match method.kind {
811 MethodKind::Constructor => quote!(#[new]),
812 MethodKind::Static => quote!(#[staticmethod]),
813 MethodKind::Factory => quote!(#[staticmethod]),
814 _ => quote!(),
815 };
816
817 if !matches!(method.kind, MethodKind::ToString) {
818 pyo3_attr = quote! {
819 #pyo3_attr
820 #[pyo3(signature = (#(#arg_idents),*))]
821 };
822 }
823
824 let remapped_method_ident = if matches!(method.kind, MethodKind::ToString) {
825 Ident::new("__str__", Span::mixed_site())
826 } else {
827 method_ident.clone()
828 };
829
830 match method.kind {
831 MethodKind::Constructor | MethodKind::Static | MethodKind::Factory => {
832 method_tokens.extend(quote! {
833 #pyo3_attr
834 pub fn #remapped_method_ident(
835 #( #arg_idents: #arg_types ),*
836 ) -> pyo3::PyResult<#ret> {
837 Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
838 #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
839 )?, &bindy::Pyo3Context)?)
840 }
841 });
842 }
843 MethodKind::Normal | MethodKind::ToString => {
844 method_tokens.extend(quote! {
845 #pyo3_attr
846 pub fn #remapped_method_ident(
847 &self,
848 #( #arg_idents: #arg_types ),*
849 ) -> pyo3::PyResult<#ret> {
850 Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#fully_qualified_ident::#method_ident(
851 &self.0,
852 #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
853 )?, &bindy::Pyo3Context)?)
854 }
855 });
856 }
857 MethodKind::Async => {
858 method_tokens.extend(quote! {
859 #pyo3_attr
860 pub fn #remapped_method_ident<'a>(
861 &self,
862 py: Python<'a>,
863 #( #arg_idents: #arg_types ),*
864 ) -> pyo3::PyResult<pyo3::Bound<'a, pyo3::PyAny>> {
865 let clone_of_self = self.0.clone();
866 #( let #arg_idents = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?; )*
867
868 pyo3_async_runtimes::tokio::future_into_py(py, async move {
869 let result: pyo3::PyResult<#ret> = Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(clone_of_self.#method_ident(
870 #( #arg_idents ),*
871 ).await?, &bindy::Pyo3Context)?);
872 result
873 })
874 }
875 });
876 }
877 }
878 }
879
880 let mut field_tokens = quote!();
881
882 for (name, ty) in fields {
883 let ident = Ident::new(name, Span::mixed_site());
884 let get_ident = Ident::new(&format!("get_{name}"), Span::mixed_site());
885 let set_ident = Ident::new(&format!("set_{name}"), Span::mixed_site());
886 let ty = parse_str::<Type>(apply_mappings(ty, &mappings).as_str()).unwrap();
887
888 field_tokens.extend(quote! {
889 #[getter(#ident)]
890 pub fn #get_ident(&self) -> pyo3::PyResult<#ty> {
891 Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(self.0.#ident.clone(), &bindy::Pyo3Context)?)
892 }
893
894 #[setter(#ident)]
895 pub fn #set_ident(&mut self, value: #ty) -> pyo3::PyResult<()> {
896 self.0.#ident = bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(value, &bindy::Pyo3Context)?;
897 Ok(())
898 }
899 });
900 }
901
902 if *new {
903 let arg_idents = fields
904 .keys()
905 .map(|k| Ident::new(k, Span::mixed_site()))
906 .collect::<Vec<_>>();
907
908 let arg_types = fields
909 .values()
910 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
911 .collect::<Vec<_>>();
912
913 method_tokens.extend(quote! {
914 #[new]
915 #[pyo3(signature = (#(#arg_idents),*))]
916 pub fn new(
917 #( #arg_idents: #arg_types ),*
918 ) -> pyo3::PyResult<Self> {
919 Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#rust_struct_ident {
920 #(#arg_idents: bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)?),*
921 }, &bindy::Pyo3Context)?)
922 }
923 });
924 }
925
926 output.extend(quote! {
927 #[pyo3::pyclass]
928 #[derive(Clone)]
929 pub struct #bound_ident(#rust_struct_ident);
930
931 #[pyo3::pymethods]
932 impl #bound_ident {
933 #method_tokens
934 #field_tokens
935 }
936
937 impl<T> bindy::FromRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
938 fn from_rust(value: #rust_struct_ident, _context: &T) -> bindy::Result<Self> {
939 Ok(Self(value))
940 }
941 }
942
943 impl<T> bindy::IntoRust<#rust_struct_ident, T, bindy::Pyo3> for #bound_ident {
944 fn into_rust(self, _context: &T) -> bindy::Result<#rust_struct_ident> {
945 Ok(self.0)
946 }
947 }
948 });
949 }
950 Binding::Enum { values } => {
951 let bound_ident = Ident::new(&name, Span::mixed_site());
952 let rust_ident = quote!( #entrypoint::#bound_ident );
953
954 let value_idents = values
955 .iter()
956 .map(|v| Ident::new(v, Span::mixed_site()))
957 .collect::<Vec<_>>();
958
959 output.extend(quote! {
960 #[pyo3::pyclass(eq, eq_int)]
961 #[derive(Clone, PartialEq, Eq)]
962 pub enum #bound_ident {
963 #( #value_idents ),*
964 }
965
966 impl<T> bindy::FromRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
967 fn from_rust(value: #rust_ident, _context: &T) -> bindy::Result<Self> {
968 Ok(match value {
969 #( #rust_ident::#value_idents => Self::#value_idents ),*
970 })
971 }
972 }
973
974 impl<T> bindy::IntoRust<#rust_ident, T, bindy::Pyo3> for #bound_ident {
975 fn into_rust(self, _context: &T) -> bindy::Result<#rust_ident> {
976 Ok(match self {
977 #( Self::#value_idents => #rust_ident::#value_idents ),*
978 })
979 }
980 }
981 });
982 }
983 Binding::Function { args, ret } => {
984 let arg_idents = args
985 .keys()
986 .map(|k| Ident::new(k, Span::mixed_site()))
987 .collect::<Vec<_>>();
988
989 let arg_types = args
990 .values()
991 .map(|v| parse_str::<Type>(apply_mappings(v, &mappings).as_str()).unwrap())
992 .collect::<Vec<_>>();
993
994 let ret = parse_str::<Type>(
995 apply_mappings(ret.as_deref().unwrap_or("()"), &mappings).as_str(),
996 )
997 .unwrap();
998
999 output.extend(quote! {
1000 #[pyo3::pyfunction]
1001 #[pyo3(signature = (#(#arg_idents),*))]
1002 pub fn #bound_ident(
1003 #( #arg_idents: #arg_types ),*
1004 ) -> pyo3::PyResult<#ret> {
1005 Ok(bindy::FromRust::<_, _, bindy::Pyo3>::from_rust(#entrypoint::#bound_ident(
1006 #( bindy::IntoRust::<_, _, bindy::Pyo3>::into_rust(#arg_idents, &bindy::Pyo3Context)? ),*
1007 )?, &bindy::Pyo3Context)?)
1008 }
1009 });
1010 }
1011 }
1012
1013 match binding {
1014 Binding::Class { .. } => {
1015 module.extend(quote! {
1016 m.add_class::<#bound_ident>()?;
1017 });
1018 }
1019 Binding::Enum { .. } => {
1020 module.extend(quote! {
1021 m.add_class::<#bound_ident>()?;
1022 });
1023 }
1024 Binding::Function { .. } => {
1025 module.extend(quote! {
1026 m.add_function(pyo3::wrap_pyfunction!(#bound_ident, m)?)?;
1027 });
1028 }
1029 }
1030 }
1031
1032 let pymodule = Ident::new(&bindy.pymodule, Span::mixed_site());
1033
1034 output.extend(quote! {
1035 #[pyo3::pymodule]
1036 fn #pymodule(m: &pyo3::Bound<'_, pyo3::prelude::PyModule>) -> pyo3::PyResult<()> {
1037 use pyo3::types::PyModuleMethods;
1038 #module
1039 Ok(())
1040 }
1041 });
1042
1043 output.into()
1044}
1045
1046fn apply_mappings(ty: &str, mappings: &IndexMap<String, String>) -> String {
1047 if let Some(mapped) = mappings.get(ty) {
1049 return mapped.clone();
1050 }
1051
1052 if let (Some(start), Some(end)) = (ty.find('<'), ty.rfind('>')) {
1054 let base_type = &ty[..start];
1055 let generic_part = &ty[start + 1..end];
1056
1057 let generic_params: Vec<&str> = generic_part.split(',').map(|s| s.trim()).collect();
1059
1060 let mapped_params: Vec<String> = generic_params
1062 .into_iter()
1063 .map(|param| apply_mappings(param, mappings))
1064 .collect();
1065
1066 let mapped_base = mappings
1068 .get(base_type)
1069 .map(|s| s.as_str())
1070 .unwrap_or(base_type);
1071
1072 format!("{}<{}>", mapped_base, mapped_params.join(", "))
1074 } else {
1075 ty.to_string()
1077 }
1078}