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