use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, ItemStruct, Meta, parse::Parse, parse::ParseStream, LitStr, Token};
#[proc_macro_attribute]
pub fn hush_op(attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_name_str = fn_name.to_string();
let mut is_generator = false;
let mut custom_name: Option<String> = None;
if !attr.is_empty() {
let meta_list: syn::punctuated::Punctuated<Meta, syn::Token![,]> =
parse_macro_input!(attr with syn::punctuated::Punctuated::parse_terminated);
for meta in &meta_list {
match meta {
Meta::Path(path) if path.is_ident("generator") => {
is_generator = true;
}
Meta::NameValue(nv) if nv.path.is_ident("name") => {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = &nv.value
{
custom_name = Some(s.value());
}
}
_ => {}
}
}
}
let op_name = custom_name.unwrap_or(fn_name_str);
let is_typed = is_typed_signature(&input_fn);
let (call_expr, wrapper_fn) = if is_typed {
generate_typed_wrapper(&input_fn, &op_name)
} else {
let call = quote! { |v| #fn_name(v) };
(call, quote! {})
};
let submit = if is_generator {
quote! {
::inventory::submit! {
::hush_serve::OpEntry::new_gen(#op_name, module_path!(), #call_expr)
}
}
} else {
quote! {
::inventory::submit! {
::hush_serve::OpEntry::new_op(#op_name, module_path!(), #call_expr)
}
}
};
let output = quote! {
#input_fn
#wrapper_fn
#submit
};
output.into()
}
fn is_typed_signature(func: &ItemFn) -> bool {
let params: Vec<_> = func.sig.inputs.iter().collect();
if params.len() == 1 {
if let syn::FnArg::Typed(pat_type) = ¶ms[0] {
if is_ref_to_value(&pat_type.ty) {
return false; }
}
}
!params.is_empty()
}
fn is_ref_to_value(ty: &syn::Type) -> bool {
if let syn::Type::Reference(r) = ty {
return is_value_type(&r.elem);
}
false
}
fn is_value_type(ty: &syn::Type) -> bool {
match ty {
syn::Type::Path(tp) => {
let segments: Vec<_> = tp.path.segments.iter().collect();
match segments.len() {
1 => segments[0].ident == "Value",
2 => segments[0].ident == "serde_json" && segments[1].ident == "Value",
_ => false,
}
}
_ => false,
}
}
fn generate_typed_wrapper(func: &ItemFn, _op_name: &str) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
let fn_name = &func.sig.ident;
let wrapper_name = syn::Ident::new(
&format!("__hush_{}_wrapper", fn_name),
fn_name.span(),
);
let mut deserialize_stmts = Vec::new();
let mut call_args = Vec::new();
for arg in &func.sig.inputs {
if let syn::FnArg::Typed(pat_type) = arg {
if let syn::Pat::Ident(ident) = &*pat_type.pat {
let param_name = &ident.ident;
let param_name_str = param_name.to_string();
let param_type = &pat_type.ty;
deserialize_stmts.push(quote! {
let #param_name: #param_type = match ::serde_json::from_value(
__inputs.get(#param_name_str).cloned().unwrap_or(::serde_json::Value::Null)
) {
Ok(v) => v,
Err(e) => return ::serde_json::json!({
"error": format!("hush_op '{}': param '{}': {}", stringify!(#fn_name), #param_name_str, e)
}),
};
});
call_args.push(quote! { #param_name });
}
}
}
let is_value_return = match &func.sig.output {
syn::ReturnType::Default => true,
syn::ReturnType::Type(_, ty) => is_value_type(ty),
};
let call_and_return = if is_value_return {
quote! { #fn_name(#(#call_args),*) }
} else {
quote! {
let __result = #fn_name(#(#call_args),*);
::serde_json::to_value(__result).unwrap_or_else(|e| ::serde_json::json!({
"error": format!("hush_op '{}': failed to serialize return: {}", stringify!(#fn_name), e)
}))
}
};
let wrapper = quote! {
fn #wrapper_name(__inputs: &::serde_json::Value) -> ::serde_json::Value {
#(#deserialize_stmts)*
#call_and_return
}
};
let call_expr = quote! { |v| #wrapper_name(v) };
(call_expr, wrapper)
}
struct ResourceArgs {
name: String,
}
impl Parse for ResourceArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let _ident: syn::Ident = input.parse()?;
let _eq: Token![=] = input.parse()?;
let name: LitStr = input.parse()?;
Ok(ResourceArgs { name: name.value() })
}
}
#[proc_macro_attribute]
pub fn hush_resource(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as ResourceArgs);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let resource_name = &args.name;
let submit = quote! {
::inventory::submit! {
::hush_serve::ResourceEntry::new(#resource_name, |config| {
Box::new(#fn_name(config)) as Box<dyn ::std::any::Any + Send + Sync>
})
}
};
let output = quote! {
#input_fn
#submit
};
output.into()
}
#[proc_macro_attribute]
pub fn hush_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_struct = parse_macro_input!(item as ItemStruct);
let output = quote! {
#[derive(::serde::Serialize, ::serde::Deserialize, Debug, Clone)]
#input_struct
};
output.into()
}