Skip to main content

bridgerust_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    DeriveInput, ImplItem, ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, parse_macro_input,
5};
6
7// Helper to parse arguments like #[export(object)]
8struct 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/// Shorthand for #[export] - more ergonomic
31#[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            // Recursively process nested modules
155            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/// Enhanced export macro that generates bindings for both Python and Node.js
175#[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    // Parse attributes
182    let args = syn::parse_macro_input!(attr as ExportArgs);
183
184    // Try to parse as function first
185    if let Ok(input_fn) = syn::parse::<ItemFn>(item.clone()) {
186        return export_function(input_fn);
187    }
188
189    // Try to parse as struct
190    if let Ok(input_struct) = syn::parse::<ItemStruct>(item.clone()) {
191        return export_struct(input_struct, args);
192    }
193
194    // Try to parse as enum
195    if let Ok(input_enum) = syn::parse::<ItemEnum>(item.clone()) {
196        return export_enum(input_enum);
197    }
198
199    // Try to parse as impl block (methods)
200    if let Ok(input_impl) = syn::parse::<ItemImpl>(item.clone()) {
201        return export_impl(input_impl);
202    }
203
204    // If none work, try as generic item and provide helpful error
205    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    // Validate function visibility
230    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    // Check for generic type parameters
240    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    // Check if return type is an iterator
252    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    // Simplified check
349    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    // Prepare fields for Python (with PyO3 attrs)
417    let mut fields_py = syn::punctuated::Punctuated::<syn::Field, syn::token::Comma>::new();
418    // Prepare fields for Node (without PyO3 attrs)
419    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            // Base attributes (common)
428            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                // Python Field
440                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                // Check for validation
449                for attr in &field.attrs {
450                    if attr.path().is_ident("validate") {
451                        // TODO: Generate validation logic
452                        // parsing attr args is hard here without full context.
453                        // For now we just allow the attribute to exist.
454                    }
455                }
456                // Add Napi attrs if needed (for mixed builds)
457                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                // Node Field
463                let mut fn_ = field.clone();
464                fn_.attrs = base_attrs.clone();
465                // No PyO3 attrs
466                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                // Private: add to both
472                let mut fp = field.clone();
473                fp.attrs = base_attrs.clone(); // Strip readonly if accidentally on private?
474                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    // Construct Python Struct
484    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    // Construct Node Struct
490    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/// Export a struct as a cross-platform exception.
630///
631/// Usage: `#[bridgerust::exception(module = "my_module")]`
632#[proc_macro_attribute]
633pub fn exception(attr: TokenStream, item: TokenStream) -> TokenStream {
634    exception::export_exception(attr, item)
635}