1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 DeriveInput, ImplItem, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, parse_macro_input,
5};
6
7struct ExportArgs {
9 is_object: bool,
10}
11
12impl syn::parse::Parse for ExportArgs {
13 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
14 let mut is_object = false;
15
16 if !input.is_empty() {
17 let vars =
18 syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated(input)?;
19 for var in vars {
20 if var == "object" {
21 is_object = true;
22 }
23 }
24 }
25
26 Ok(ExportArgs { is_object })
27 }
28}
29
30#[proc_macro_attribute]
32pub fn bridge(attr: TokenStream, item: TokenStream) -> TokenStream {
33 process_export(attr, item)
34}
35
36#[proc_macro_attribute]
37pub fn validate(attr: TokenStream, item: TokenStream) -> TokenStream {
38 let attrs = parse_macro_input!(
39 attr with syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
40 );
41
42 for meta in attrs {
43 match meta {
44 syn::Meta::Path(path) => {
45 let is_supported =
46 path.is_ident("required") || path.is_ident("email") || path.is_ident("url");
47 if !is_supported {
48 return syn::Error::new_spanned(
49 path,
50 "Unsupported #[validate] flag. Supported flags: required, email, url",
51 )
52 .to_compile_error()
53 .into();
54 }
55 }
56 syn::Meta::NameValue(nv) => {
57 let key = nv
58 .path
59 .get_ident()
60 .map(ToString::to_string)
61 .unwrap_or_default();
62 let is_supported = matches!(key.as_str(), "min" | "max" | "len" | "pattern");
63 if !is_supported {
64 return syn::Error::new_spanned(
65 nv.path,
66 "Unsupported #[validate] key. Supported keys: min, max, len, pattern",
67 )
68 .to_compile_error()
69 .into();
70 }
71
72 if key == "pattern"
73 && !matches!(
74 &nv.value,
75 syn::Expr::Lit(syn::ExprLit {
76 lit: syn::Lit::Str(_),
77 ..
78 })
79 )
80 {
81 return syn::Error::new_spanned(
82 nv.value,
83 "#[validate(pattern = ...)] expects a string literal",
84 )
85 .to_compile_error()
86 .into();
87 }
88 }
89 syn::Meta::List(list) => {
90 return syn::Error::new_spanned(
91 list,
92 "Unsupported #[validate(...)] list form. Use flags (required) or key/value pairs (min = 1)",
93 )
94 .to_compile_error()
95 .into();
96 }
97 }
98 }
99
100 item
101}
102
103#[proc_macro_attribute]
104pub fn bridge_async(_attr: TokenStream, item: TokenStream) -> TokenStream {
105 let input_fn = parse_macro_input!(item as ItemFn);
106
107 if input_fn.sig.asyncness.is_none() {
108 return syn::Error::new_spanned(
109 &input_fn.sig,
110 "#[bridge_async] can only be used on async functions. \
111 Either add 'async' keyword or use #[bridge] instead.",
112 )
113 .to_compile_error()
114 .into();
115 }
116
117 export_async_function(input_fn)
118}
119
120#[proc_macro_attribute]
121pub fn bridge_module(_attr: TokenStream, item: TokenStream) -> TokenStream {
122 let mut input = syn::parse_macro_input!(item as ItemMod);
123
124 if let Some((_, items)) = &mut input.content {
125 for item in items {
126 process_item_for_bridge(item);
127 }
128 }
129
130 quote!(#input).into()
131}
132
133fn process_item_for_bridge(item: &mut syn::Item) {
134 match item {
135 syn::Item::Fn(item_fn) => {
136 if matches!(item_fn.vis, syn::Visibility::Public(_)) {
137 add_bridge_if_missing(&mut item_fn.attrs);
138 }
139 }
140 syn::Item::Struct(item_struct) => {
141 if matches!(item_struct.vis, syn::Visibility::Public(_)) {
142 add_bridge_if_missing(&mut item_struct.attrs);
143 }
144 }
145 syn::Item::Enum(item_enum) => {
146 if matches!(item_enum.vis, syn::Visibility::Public(_)) {
147 add_bridge_if_missing(&mut item_enum.attrs);
148 }
149 }
150 syn::Item::Impl(item_impl) => {
151 add_bridge_if_missing(&mut item_impl.attrs);
152 }
153 syn::Item::Mod(item_mod) => {
154 if let Some((_, items)) = &mut item_mod.content {
156 for nested_item in items {
157 process_item_for_bridge(nested_item);
158 }
159 }
160 }
161 _ => {}
162 }
163}
164
165fn add_bridge_if_missing(attrs: &mut Vec<syn::Attribute>) {
166 let has_bridge = attrs
167 .iter()
168 .any(|attr| attr.path().is_ident("bridge") || attr.path().is_ident("export"));
169 if !has_bridge {
170 attrs.push(syn::parse_quote!(#[::bridgerust::bridge]));
171 }
172}
173
174#[proc_macro_attribute]
176pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
177 process_export(attr, item)
178}
179
180fn process_export(attr: TokenStream, item: TokenStream) -> TokenStream {
181 let args = syn::parse_macro_input!(attr as ExportArgs);
183
184 if let Ok(input_fn) = syn::parse::<ItemFn>(item.clone()) {
186 return export_function(input_fn);
187 }
188
189 if let Ok(input_struct) = syn::parse::<ItemStruct>(item.clone()) {
191 return export_struct(input_struct, args);
192 }
193
194 if let Ok(input_enum) = syn::parse::<ItemEnum>(item.clone()) {
196 return export_enum(input_enum);
197 }
198
199 if let Ok(input_impl) = syn::parse::<ItemImpl>(item.clone()) {
201 return export_impl(input_impl);
202 }
203
204 let input = parse_macro_input!(item as DeriveInput);
206 syn::Error::new_spanned(
207 &input.ident,
208 "#[bridge] / #[export] can only be applied to functions, structs, enums, or impl blocks",
209 )
210 .to_compile_error()
211 .into()
212}
213
214fn helpful_error<T: quote::ToTokens>(
215 span: &T,
216 limitation: &str,
217 workaround: &str,
218 issue: Option<&str>,
219) -> TokenStream {
220 let mut msg = format!("{}\n\nWorkaround: {}", limitation, workaround);
221 if let Some(url) = issue {
222 msg.push_str(&format!("\n\nTrack support: {}", url));
223 }
224
225 syn::Error::new_spanned(span, msg).to_compile_error().into()
226}
227
228fn export_function(input_fn: ItemFn) -> TokenStream {
229 if !matches!(input_fn.vis, syn::Visibility::Public(_)) {
231 return syn::Error::new_spanned(
232 &input_fn.sig.ident,
233 "Functions exported with #[bridge] must be public (use `pub fn`)",
234 )
235 .to_compile_error()
236 .into();
237 }
238
239 if !input_fn.sig.generics.params.is_empty() {
241 return helpful_error(
242 &input_fn.sig.generics,
243 "Generic functions are not supported by BridgeRust yet.",
244 "Use dynamic dispatch with Box<dyn Trait> or enum variants instead.",
245 None,
246 );
247 }
248
249 let is_async = input_fn.sig.asyncness.is_some();
250
251 let is_iterator = if let syn::ReturnType::Type(_, return_type) = &input_fn.sig.output {
253 is_iterator_type(return_type)
254 } else {
255 false
256 };
257
258 if is_iterator && let syn::ReturnType::Type(_, return_type) = &input_fn.sig.output {
259 return helpful_error(
260 return_type,
261 "Iterator return types used in export are not yet supported.",
262 "Return Vec<T> instead.",
263 None,
264 );
265 }
266
267 if let syn::ReturnType::Type(_, return_type) = &input_fn.sig.output
268 && let Err(err) = validate_type(return_type, "return type")
269 {
270 return err;
271 }
272
273 for input in &input_fn.sig.inputs {
274 if let syn::FnArg::Typed(pat_type) = input
275 && let Err(err) = validate_type(&pat_type.ty, "parameter type")
276 {
277 return err;
278 }
279 }
280
281 if is_async {
282 export_async_function(input_fn)
283 } else {
284 let expanded = quote! {
285 #[cfg_attr(feature = "python", ::bridgerust::pyo3::pyfunction(crate = "::bridgerust::pyo3"))]
286 #[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi)]
287 #input_fn
288 };
289 TokenStream::from(expanded)
290 }
291}
292
293fn export_async_function(input_fn: ItemFn) -> TokenStream {
294 let fn_name = &input_fn.sig.ident;
295 let fn_attrs = &input_fn.attrs;
296 let fn_vis = &input_fn.vis;
297 let fn_sig = &input_fn.sig;
298 let fn_block = &input_fn.block;
299
300 let mut wrapper_params = Vec::new();
301 let mut call_args = Vec::new();
302
303 for input in &fn_sig.inputs {
304 match input {
305 syn::FnArg::Receiver(_) => {
306 return syn::Error::new_spanned(
307 fn_sig,
308 "Methods are not supported in async functions via export_function logic",
309 )
310 .to_compile_error()
311 .into();
312 }
313 syn::FnArg::Typed(pat_type) => {
314 let param_name = &pat_type.pat;
315 let param_type = &pat_type.ty;
316 wrapper_params.push(quote! { #param_name: #param_type });
317 call_args.push(quote! { #param_name });
318 }
319 }
320 }
321
322 let expanded = quote! {
323 #(#fn_attrs)*
324 #fn_vis #fn_sig #fn_block
325
326 #[cfg(feature = "nodejs")]
327 #[bridgerust::napi_derive::napi]
328 #(#fn_attrs)*
329 #fn_vis #fn_sig #fn_block
330
331 #[cfg(feature = "python")]
332 #[bridgerust::pyo3::pyfunction(crate = "bridgerust::pyo3")]
333 pub fn #fn_name(
334 py: bridgerust::pyo3::Python<'_>,
335 #(#wrapper_params),*
336 ) -> bridgerust::pyo3::PyResult<bridgerust::pyo3::PyObject> {
337 use bridgerust::pyo3::IntoPy;
338 bridgerust::pyo3_async_runtimes::tokio::future_into_py(py, async move {
339 #fn_name(#(#call_args),*).await.map_err(Into::into)
340 })
341 }
342 };
343
344 TokenStream::from(expanded)
345}
346
347fn is_iterator_type(ty: &syn::Type) -> bool {
348 if let syn::Type::Path(type_path) = ty
350 && let Some(segment) = type_path.path.segments.last()
351 {
352 let name = segment.ident.to_string();
353 return name == "Iterator" || name == "IntoIterator";
354 }
355 false
356}
357
358fn validate_type(ty: &syn::Type, _context: &str) -> Result<(), TokenStream> {
359 if let syn::Type::Path(type_path) = ty
360 && let Some(segment) = type_path.path.segments.last()
361 {
362 let name = segment.ident.to_string();
363 if matches!(name.as_str(), "HashMap" | "HashSet" | "BTreeMap") {
364 return Err(helpful_error(
365 ty,
366 &format!("Type {} requires a wrapper for cross-language use.", name),
367 &format!(
368 "Use bridgerust::collections::{}Wrapper or convert to Vec<(K,V)>.\n\
369 \n\
370 Example:\n\
371 use bridgerust::collections::HashMapWrapper;\n\
372 \n\
373 #[bridge]\n\
374 pub fn get_map() -> HashMapWrapper<String, i32> {{\n\
375 // ...\n\
376 }}\n\
377 \n\
378 Or convert:\n\
379 pub fn get_map() -> Vec<(String, i32)> {{\n\
380 my_hashmap.into_iter().collect()\n\
381 }}",
382 name
383 ),
384 None,
385 ));
386 }
387 if (name == "Vec" || name == "Option")
388 && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
389 && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
390 {
391 return validate_type(inner, _context);
392 }
393 }
394 Ok(())
395}
396
397fn export_struct(mut input_struct: ItemStruct, args: ExportArgs) -> TokenStream {
398 if !matches!(input_struct.vis, syn::Visibility::Public(_)) {
399 return syn::Error::new_spanned(&input_struct.ident, "Structs exported must be public")
400 .to_compile_error()
401 .into();
402 }
403
404 if !input_struct.generics.params.is_empty() {
405 return syn::Error::new_spanned(&input_struct.generics, "Generics not supported")
406 .to_compile_error()
407 .into();
408 }
409
410 if let syn::Fields::Unnamed(_) = &input_struct.fields {
411 return syn::Error::new_spanned(&input_struct.ident, "Tuple structs not supported")
412 .to_compile_error()
413 .into();
414 }
415
416 let mut fields_py = syn::punctuated::Punctuated::<syn::Field, syn::token::Comma>::new();
418 let mut fields_node = syn::punctuated::Punctuated::<syn::Field, syn::token::Comma>::new();
420
421 if let syn::Fields::Named(fields) = &mut input_struct.fields {
422 for field in &mut fields.named {
423 if let Err(err) = validate_type(&field.ty, "struct field") {
424 return err;
425 }
426
427 let mut base_attrs = Vec::new();
429 let mut is_readonly = false;
430 for attr in &field.attrs {
431 if attr.path().is_ident("readonly") {
432 is_readonly = true;
433 } else {
434 base_attrs.push(attr.clone());
435 }
436 }
437
438 if matches!(field.vis, syn::Visibility::Public(_)) {
439 let mut fp = field.clone();
441 fp.attrs = base_attrs.clone();
442 if is_readonly {
443 fp.attrs.push(syn::parse_quote!(#[pyo3(get)]));
444 } else {
445 fp.attrs.push(syn::parse_quote!(#[pyo3(get, set)]));
446 }
447
448 for attr in &field.attrs {
450 if attr.path().is_ident("validate") {
451 }
455 }
456 if is_readonly && !args.is_object {
458 fp.attrs.push(syn::parse_quote!(#[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi(readonly))]));
459 }
460 fields_py.push(fp);
461
462 let mut fn_ = field.clone();
464 fn_.attrs = base_attrs.clone();
465 if is_readonly && !args.is_object {
467 fn_.attrs.push(syn::parse_quote!(#[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi(readonly))]));
468 }
469 fields_node.push(fn_);
470 } else {
471 let mut fp = field.clone();
473 fp.attrs = base_attrs.clone(); fields_py.push(fp);
475
476 let mut fn_ = field.clone();
477 fn_.attrs = base_attrs.clone();
478 fields_node.push(fn_);
479 }
480 }
481 }
482
483 let mut struct_py = input_struct.clone();
485 if let syn::Fields::Named(f) = &mut struct_py.fields {
486 f.named = fields_py;
487 }
488
489 let mut struct_node = input_struct.clone();
491 if let syn::Fields::Named(f) = &mut struct_node.fields {
492 f.named = fields_node;
493 }
494
495 let napi_attr = if args.is_object {
496 quote! { #[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi(object))] }
497 } else {
498 quote! { #[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi)] }
499 };
500
501 let expanded = quote! {
502 #[cfg(feature = "python")]
503 #[::bridgerust::pyo3::pyclass(crate = "::bridgerust::pyo3")]
504 #napi_attr
505 #struct_py
506
507 #[cfg(not(feature = "python"))]
508 #napi_attr
509 #struct_node
510 };
511 TokenStream::from(expanded)
512}
513
514fn export_enum(input_enum: ItemEnum) -> TokenStream {
515 if !matches!(input_enum.vis, syn::Visibility::Public(_)) {
516 return syn::Error::new_spanned(&input_enum.ident, "Enums must be public")
517 .to_compile_error()
518 .into();
519 }
520 if !input_enum.generics.params.is_empty() {
521 return syn::Error::new_spanned(&input_enum.generics, "Generic enums not supported")
522 .to_compile_error()
523 .into();
524 }
525
526 let expanded = quote! {
527 #[cfg_attr(feature = "python", ::bridgerust::pyo3::pyclass(crate = "::bridgerust::pyo3"))]
528 #[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi)]
529 #input_enum
530 };
531 TokenStream::from(expanded)
532}
533
534fn export_impl(mut input_impl: ItemImpl) -> TokenStream {
535 if input_impl.trait_.is_some() {
536 return syn::Error::new_spanned(input_impl.self_ty, "Trait impls not supported")
537 .to_compile_error()
538 .into();
539 }
540
541 for item in &mut input_impl.items {
542 if let ImplItem::Fn(method) = item {
543 let mut is_constructor = false;
544 let mut new_attrs = Vec::new();
545 for attr in &method.attrs {
546 if attr.path().is_ident("constructor") {
547 is_constructor = true;
548 } else {
549 new_attrs.push(attr.clone());
550 }
551 }
552 method.attrs = new_attrs;
553
554 if is_constructor {
555 method.attrs.push(syn::parse_quote!(#[new]));
556 method.attrs.push(syn::parse_quote!(#[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi(constructor))]));
557 } else {
558 let has_self = method
559 .sig
560 .inputs
561 .iter()
562 .any(|arg| matches!(arg, syn::FnArg::Receiver(_)));
563 if !has_self {
564 method.attrs.push(syn::parse_quote!(#[staticmethod]));
565 }
566 }
567 }
568 }
569
570 let expanded = quote! {
571 const _: () = {
572 #[allow(unused_imports)]
573 use ::bridgerust::{new, staticmethod};
574
575 #[cfg_attr(feature = "python", ::bridgerust::pyo3::pymethods(crate = "::bridgerust::pyo3"))]
576 #[cfg_attr(feature = "nodejs", ::bridgerust::napi_derive::napi)]
577 #input_impl
578 };
579 };
580 TokenStream::from(expanded)
581}
582
583#[proc_macro_attribute]
584pub fn error(_attr: TokenStream, item: TokenStream) -> TokenStream {
585 let input = parse_macro_input!(item as DeriveInput);
586 let enum_name = &input.ident;
587 if !matches!(input.data, syn::Data::Enum(_)) {
588 return syn::Error::new_spanned(&input.ident, "error macro only for enums")
589 .to_compile_error()
590 .into();
591 }
592
593 let expanded = quote! {
594 #input
595 #[cfg(feature = "python")]
596 #[allow(unexpected_cfgs)]
597 pub fn to_py_err(err: #enum_name) -> bridgerust::pyo3::PyErr {
598 use std::fmt::Display;
599 bridgerust::pyo3::exceptions::PyRuntimeError::new_err(err.to_string())
600 }
601
602 #[cfg(feature = "nodejs")]
603 #[allow(unexpected_cfgs)]
604 pub fn to_napi_err(err: #enum_name) -> bridgerust::napi::Error {
605 use std::fmt::Display;
606 bridgerust::napi::Error::from_reason(err.to_string())
607 }
608 };
609 TokenStream::from(expanded)
610}
611
612#[proc_macro_attribute]
613pub fn new(_attr: TokenStream, item: TokenStream) -> TokenStream {
614 item
615}
616
617#[proc_macro_attribute]
618pub fn staticmethod(_attr: TokenStream, item: TokenStream) -> TokenStream {
619 item
620}
621
622#[proc_macro_attribute]
623pub fn pyo3_dummy(_attr: TokenStream, item: TokenStream) -> TokenStream {
624 item
625}
626
627mod exception;
628
629#[proc_macro_attribute]
633pub fn exception(attr: TokenStream, item: TokenStream) -> TokenStream {
634 exception::export_exception(attr, item)
635}