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