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