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