use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, FnArg, ItemFn, Pat, ReturnType, Type};
fn get_crate_token() -> proc_macro2::TokenStream {
match std::env::var("CARGO_PKG_NAME") {
Ok(ref name) if name == "autobahn-client" || name == "autobahn_client" => {
quote::quote!(crate)
}
_ => quote::quote!(autobahn_client),
}
}
#[proc_macro_attribute]
pub fn server_function(_args: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let fn_name = &input_fn.sig.ident;
let _vis = &input_fn.vis;
let inputs = &input_fn.sig.inputs;
let output = &input_fn.sig.output;
let mut arg_types: Vec<syn::Type> = Vec::new();
for arg in inputs.iter() {
match arg {
FnArg::Typed(pat_type) => {
if is_primitive_type(&pat_type.ty) {
panic!("Primitive types are not allowed as RPC parameters. Type '{}' is a primitive type. Only protobuf message types are supported.",
quote::quote!(#pat_type.ty));
}
arg_types.push((*pat_type.ty).clone());
}
FnArg::Receiver(_) => panic!("Methods with `self` are not supported"),
}
}
let crate_token = get_crate_token();
let input_type = if arg_types.is_empty() {
quote! { #crate_token::proto::autobahn::RpcEmpty }
} else {
let ty = &arg_types[0];
quote! { #ty }
};
let output_type = match output {
ReturnType::Default => quote! { #crate_token::proto::autobahn::RpcEmpty },
ReturnType::Type(_, ty) => quote! { #ty },
};
let rpc_name = from_function_to_rpc_name(&input_fn);
let topic = format!("RPC/FUNCTIONAL_SERVICE/{}", rpc_name);
let register_fn_name = format_ident!("__register_{}", fn_name);
let wrapper_fn_name = format_ident!("{}_wrapper", fn_name);
let (wrapper_params, fn_call) = if arg_types.is_empty() {
(quote! { _input: #input_type }, quote! { #fn_name().await })
} else {
(
quote! { input: #input_type },
quote! { #fn_name(input).await },
)
};
let return_handling = match output {
ReturnType::Default => {
quote! {
#fn_call;
Ok(#crate_token::proto::autobahn::RpcEmpty::default())
}
}
ReturnType::Type(_, _) => {
quote! {
Ok(#fn_call)
}
}
};
let gen = quote! {
#input_fn
async fn #wrapper_fn_name(
#wrapper_params
) -> ::std::result::Result<#output_type, Box<dyn ::std::error::Error + Send + Sync>> {
#return_handling
}
#[ctor::ctor]
fn #register_fn_name() {
#crate_token::rpc::server::register_server_function::<#input_type, #output_type, _, _>(
#topic.to_string(),
#wrapper_fn_name,
);
}
};
gen.into()
}
#[proc_macro_attribute]
pub fn client_function(_args: TokenStream, input: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(input as ItemFn);
let original_fn_name = &input_fn.sig.ident;
let vis = &input_fn.vis;
let inputs = &input_fn.sig.inputs;
let output = &input_fn.sig.output;
let mut arg_idents: Vec<syn::Ident> = Vec::new();
let mut arg_types: Vec<syn::Type> = Vec::new();
for arg in inputs.iter() {
match arg {
FnArg::Typed(pat_type) => match &*pat_type.pat {
Pat::Ident(pat_ident) => {
if is_primitive_type(&pat_type.ty) {
panic!("Primitive types are not allowed as RPC parameters. Type '{}' is a primitive type. Only protobuf message types are supported.",
quote::quote!(#pat_type.ty));
}
arg_idents.push(pat_ident.ident.clone());
arg_types.push((*pat_type.ty).clone());
}
_ => panic!("Parameters must be simple identifiers (e.g., `x: T`)"),
},
FnArg::Receiver(_) => panic!("Methods with `self` are not supported"),
}
}
let crate_token = get_crate_token();
let output_type = match output {
ReturnType::Default => quote! { #crate_token::proto::autobahn::RpcEmpty },
ReturnType::Type(_, ty) => quote! { #ty },
};
let impl_fn_name = format_ident!("{}_impl", original_fn_name);
let mut impl_fn = input_fn.clone();
let mut impl_sig = impl_fn.sig.clone();
impl_sig.ident = impl_fn_name.clone();
impl_fn.sig = impl_sig;
let wrapper_params = if inputs.is_empty() {
quote! { client: &::std::sync::Arc<#crate_token::autobahn::Autobahn>, timeout_ms: u64 }
} else {
quote! { client: &::std::sync::Arc<#crate_token::autobahn::Autobahn>, timeout_ms: u64, #inputs }
};
let rpc_name = from_function_to_rpc_name(&input_fn);
let (args_handling, input_type) = if arg_idents.is_empty() {
(
quote! { #crate_token::proto::autobahn::RpcEmpty::default() },
quote! { #crate_token::proto::autobahn::RpcEmpty },
)
} else if arg_idents.len() == 1 {
let first_arg = &arg_idents[0];
let first_type = &arg_types[0];
(quote! { #first_arg }, quote! { #first_type })
} else {
panic!("Multiple arguments not yet supported")
};
let output_assert = quote! {
const _: fn() = || {
fn assert_prost_message<T: ::prost::Message + ::std::default::Default>() {}
let _ = assert_prost_message::<#output_type>;
};
};
let input_assert = if arg_types.is_empty() {
quote! {} } else {
quote! {
const _: fn() = || {
#(
fn assert_valid_rpc_type<T: ::prost::Message + ::std::default::Default + 'static>() {}
let _ = assert_valid_rpc_type::<#arg_types>;
)*
};
}
};
let gen = quote! {
#output_assert
#input_assert
#impl_fn
#vis async fn #original_fn_name(
#wrapper_params
) -> ::std::result::Result<#output_type, #crate_token::rpc::RPCError> {
let args = #args_handling;
client.call_rpc::<#output_type, #input_type>(
#rpc_name.to_string(),
args,
timeout_ms,
).await
}
};
gen.into()
}
fn from_function_to_rpc_name(func: &syn::ItemFn) -> String {
use syn::{FnArg, ReturnType, Type};
let fn_name = func.sig.ident.to_string();
let mut param_types = Vec::new();
for input in &func.sig.inputs {
match input {
FnArg::Typed(pat_type) => {
let ty = &*pat_type.ty;
let type_name = match ty {
Type::Path(type_path) => type_path
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| format!("{:?}", ty)),
_ => format!("{:?}", ty),
};
param_types.push(type_name);
}
FnArg::Receiver(_) => {}
}
}
if func.sig.inputs.is_empty() {
param_types.push("None".to_string());
}
let return_type = match &func.sig.output {
ReturnType::Default => "None".to_string(),
ReturnType::Type(_, ty) => match &**ty {
Type::Path(type_path) => type_path
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| format!("{:?}", ty)),
_ => format!("{:?}", ty),
},
};
format!("{}{}{}", fn_name, param_types.join(""), return_type)
}
fn is_primitive_type(ty: &syn::Type) -> bool {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
let type_name = segment.ident.to_string();
matches!(
type_name.as_str(),
"bool"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "f32"
| "f64"
| "char"
| "str"
| "String"
)
} else {
false
}
}
Type::Reference(type_ref) => {
is_primitive_type(&type_ref.elem)
}
_ => false,
}
}