Skip to main content

autobahn_client_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{parse_macro_input, FnArg, ItemFn, Pat, ReturnType, Type};
4
5fn get_crate_token() -> proc_macro2::TokenStream {
6    match std::env::var("CARGO_PKG_NAME") {
7        Ok(ref name) if name == "autobahn-client" || name == "autobahn_client" => {
8            quote::quote!(crate)
9        }
10        _ => quote::quote!(autobahn_client),
11    }
12}
13
14/// Attribute macro to register a function as an RPC server handler.
15/// This macro generates a function that can handle RPC requests with proper type safety
16/// and registers it with the server function registry.
17#[proc_macro_attribute]
18pub fn server_function(_args: TokenStream, input: TokenStream) -> TokenStream {
19    let input_fn = parse_macro_input!(input as ItemFn);
20    let fn_name = &input_fn.sig.ident;
21    let _vis = &input_fn.vis;
22    let inputs = &input_fn.sig.inputs;
23    let output = &input_fn.sig.output;
24
25    // Extract argument types for register_server_function
26    let mut arg_types: Vec<syn::Type> = Vec::new();
27    for arg in inputs.iter() {
28        match arg {
29            FnArg::Typed(pat_type) => {
30                if is_primitive_type(&pat_type.ty) {
31                    panic!("Primitive types are not allowed as RPC parameters. Type '{}' is a primitive type. Only protobuf message types are supported.",
32                        quote::quote!(#pat_type.ty));
33                }
34                arg_types.push((*pat_type.ty).clone());
35            }
36            FnArg::Receiver(_) => panic!("Methods with `self` are not supported"),
37        }
38    }
39
40    // Use crate root resolution
41    let crate_token = get_crate_token();
42
43    // Determine input and output types
44    let input_type = if arg_types.is_empty() {
45        quote! { #crate_token::proto::autobahn::RpcEmpty }
46    } else {
47        let ty = &arg_types[0];
48        quote! { #ty }
49    };
50    let output_type = match output {
51        ReturnType::Default => quote! { #crate_token::proto::autobahn::RpcEmpty },
52        ReturnType::Type(_, ty) => quote! { #ty },
53    };
54
55    // Generate RPC name using the same logic as client_function
56    let rpc_name = from_function_to_rpc_name(&input_fn);
57    let topic = format!("RPC/FUNCTIONAL_SERVICE/{}", rpc_name);
58
59    let register_fn_name = format_ident!("__register_{}", fn_name);
60
61    // Create wrapper function that handles the Result<T, Box<dyn Error + Send + Sync>> conversion
62    let wrapper_fn_name = format_ident!("{}_wrapper", fn_name);
63
64    // Build the wrapper function parameters and call
65    let (wrapper_params, fn_call) = if arg_types.is_empty() {
66        (quote! { _input: #input_type }, quote! { #fn_name().await })
67    } else {
68        (
69            quote! { input: #input_type },
70            quote! { #fn_name(input).await },
71        )
72    };
73
74    // Handle return type conversion
75    let return_handling = match output {
76        ReturnType::Default => {
77            quote! {
78                #fn_call;
79                Ok(#crate_token::proto::autobahn::RpcEmpty::default())
80            }
81        }
82        ReturnType::Type(_, _) => {
83            quote! {
84                Ok(#fn_call)
85            }
86        }
87    };
88
89    let gen = quote! {
90        // Keep the original function
91        #input_fn
92
93        // Create a wrapper function that matches the expected signature
94        async fn #wrapper_fn_name(
95            #wrapper_params
96        ) -> ::std::result::Result<#output_type, Box<dyn ::std::error::Error + Send + Sync>> {
97            #return_handling
98        }
99
100        #[ctor::ctor]
101        fn #register_fn_name() {
102            #crate_token::rpc::server::register_server_function::<#input_type, #output_type, _, _>(
103                #topic.to_string(),
104                #wrapper_fn_name,
105            );
106        }
107    };
108
109    gen.into()
110}
111
112#[proc_macro_attribute]
113pub fn client_function(_args: TokenStream, input: TokenStream) -> TokenStream {
114    let input_fn = parse_macro_input!(input as ItemFn);
115
116    let original_fn_name = &input_fn.sig.ident;
117    let vis = &input_fn.vis;
118    let inputs = &input_fn.sig.inputs;
119    let output = &input_fn.sig.output;
120
121    // Collect argument identifiers and types
122    let mut arg_idents: Vec<syn::Ident> = Vec::new();
123    let mut arg_types: Vec<syn::Type> = Vec::new();
124
125    for arg in inputs.iter() {
126        match arg {
127            FnArg::Typed(pat_type) => match &*pat_type.pat {
128                Pat::Ident(pat_ident) => {
129                    // Validate that the type is not a primitive
130                    if is_primitive_type(&pat_type.ty) {
131                        panic!("Primitive types are not allowed as RPC parameters. Type '{}' is a primitive type. Only protobuf message types are supported.", 
132                               quote::quote!(#pat_type.ty));
133                    }
134
135                    arg_idents.push(pat_ident.ident.clone());
136                    arg_types.push((*pat_type.ty).clone());
137                }
138                _ => panic!("Parameters must be simple identifiers (e.g., `x: T`)"),
139            },
140            FnArg::Receiver(_) => panic!("Methods with `self` are not supported"),
141        }
142    }
143
144    // Use crate root resolution
145    let crate_token = get_crate_token();
146
147    let output_type = match output {
148        ReturnType::Default => quote! { #crate_token::proto::autobahn::RpcEmpty },
149        ReturnType::Type(_, ty) => quote! { #ty },
150    };
151
152    // Create an implementation function name to hold the original body
153    let impl_fn_name = format_ident!("{}_impl", original_fn_name);
154    let mut impl_fn = input_fn.clone();
155    let mut impl_sig = impl_fn.sig.clone();
156    impl_sig.ident = impl_fn_name.clone();
157    impl_fn.sig = impl_sig;
158
159    // Build wrapper parameter list
160    let wrapper_params = if inputs.is_empty() {
161        quote! { client: &::std::sync::Arc<#crate_token::autobahn::Autobahn>, timeout_ms: u64 }
162    } else {
163        quote! { client: &::std::sync::Arc<#crate_token::autobahn::Autobahn>, timeout_ms: u64, #inputs }
164    };
165
166    // Generate the RPC name string at compile time (match python: function name + arg types + return type)
167    let rpc_name = from_function_to_rpc_name(&input_fn);
168
169    // Build the argument handling with proper type bounds
170    let (args_handling, input_type) = if arg_idents.is_empty() {
171        (
172            quote! { #crate_token::proto::autobahn::RpcEmpty::default() },
173            quote! { #crate_token::proto::autobahn::RpcEmpty },
174        )
175    } else if arg_idents.len() == 1 {
176        let first_arg = &arg_idents[0];
177        let first_type = &arg_types[0];
178        (quote! { #first_arg }, quote! { #first_type })
179    } else {
180        // TODO: Implement proper multi-argument support
181        panic!("Multiple arguments not yet supported")
182    };
183
184    // Add trait bound assertions for the return type
185    let output_assert = quote! {
186        // Ensure the return type implements prost::Message + Default
187        const _: fn() = || {
188            fn assert_prost_message<T: ::prost::Message + ::std::default::Default>() {}
189            let _ = assert_prost_message::<#output_type>;
190        };
191    };
192
193    // Ensure each argument type implements prost::Message + Default (this excludes primitives)
194    let input_assert = if arg_types.is_empty() {
195        quote! {} // No assertions needed for functions with no parameters
196    } else {
197        quote! {
198            const _: fn() = || {
199                #(
200                    // This will fail to compile for primitive types, as they do not implement prost::Message
201                    fn assert_valid_rpc_type<T: ::prost::Message + ::std::default::Default + 'static>() {}
202                    let _ = assert_valid_rpc_type::<#arg_types>;
203                )*
204            };
205        }
206    };
207
208    let gen = quote! {
209        #output_assert
210        #input_assert
211
212        // Original function body moved to an internal implementation
213        #impl_fn
214
215        // Public wrapper that adds an Autobahn client and returns a Result
216        #vis async fn #original_fn_name(
217            #wrapper_params
218        ) -> ::std::result::Result<#output_type, #crate_token::rpc::RPCError> {
219            let args = #args_handling;
220
221            client.call_rpc::<#output_type, #input_type>(
222                #rpc_name.to_string(),
223                args,
224                timeout_ms,
225            ).await
226        }
227    };
228
229    gen.into()
230}
231
232/// Convert a function to an RPC name string with type information
233fn from_function_to_rpc_name(func: &syn::ItemFn) -> String {
234    use syn::{FnArg, ReturnType, Type};
235
236    let fn_name = func.sig.ident.to_string();
237
238    // Collect parameter type names
239    let mut param_types = Vec::new();
240    for input in &func.sig.inputs {
241        match input {
242            FnArg::Typed(pat_type) => {
243                let ty = &*pat_type.ty;
244                let type_name = match ty {
245                    Type::Path(type_path) => type_path
246                        .path
247                        .segments
248                        .last()
249                        .map(|seg| seg.ident.to_string())
250                        .unwrap_or_else(|| format!("{:?}", ty)),
251                    _ => format!("{:?}", ty),
252                };
253                param_types.push(type_name);
254            }
255            FnArg::Receiver(_) => {}
256        }
257    }
258
259    if func.sig.inputs.is_empty() {
260        param_types.push("None".to_string());
261    }
262
263    // Get return type name
264    let return_type = match &func.sig.output {
265        ReturnType::Default => "None".to_string(),
266        ReturnType::Type(_, ty) => match &**ty {
267            Type::Path(type_path) => type_path
268                .path
269                .segments
270                .last()
271                .map(|seg| seg.ident.to_string())
272                .unwrap_or_else(|| format!("{:?}", ty)),
273            _ => format!("{:?}", ty),
274        },
275    };
276
277    // Match Python: <function name><arg types...><return type>
278    format!("{}{}{}", fn_name, param_types.join(""), return_type)
279}
280
281/// Check if a type is a primitive type that should be rejected
282fn is_primitive_type(ty: &syn::Type) -> bool {
283    match ty {
284        Type::Path(type_path) => {
285            if let Some(segment) = type_path.path.segments.last() {
286                let type_name = segment.ident.to_string();
287                matches!(
288                    type_name.as_str(),
289                    "bool"
290                        | "i8"
291                        | "i16"
292                        | "i32"
293                        | "i64"
294                        | "i128"
295                        | "u8"
296                        | "u16"
297                        | "u32"
298                        | "u64"
299                        | "u128"
300                        | "f32"
301                        | "f64"
302                        | "char"
303                        | "str"
304                        | "String"
305                )
306            } else {
307                false
308            }
309        }
310        Type::Reference(type_ref) => {
311            // Check if it's &str or similar
312            is_primitive_type(&type_ref.elem)
313        }
314        _ => false,
315    }
316}