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