crosslic-macro 0.1.1

Proc macro helpers for the Crosslic framework
Documentation
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{AttributeArgs, FnArg, ItemFn, Lit, Meta, NestedMeta, Pat, Type, parse_macro_input};

#[proc_macro_attribute]
pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as AttributeArgs);
    let mut name_override = None;
    for arg in args {
        if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
            if nv.path.is_ident("name") {
                if let Lit::Str(litstr) = nv.lit {
                    name_override = Some(litstr.value());
                }
            }
        }
    }

    let mut input_fn = parse_macro_input!(item as ItemFn);
    let fn_name = input_fn.sig.ident.clone();
    let handler_ident = format_ident!("__crosslic_handler_{}", fn_name);
    let cmd_name = name_override.unwrap_or_else(|| fn_name.to_string());

    let params: Vec<(Box<Pat>, Box<Type>)> = input_fn
        .sig
        .inputs
        .iter()
        .filter_map(|arg| {
            if let FnArg::Typed(pat_type) = arg {
                Some((pat_type.pat.clone(), pat_type.ty.clone()))
            } else {
                None
            }
        })
        .collect();

    let param_count = params.len();
    let param_names: Vec<_> = params.iter().map(|(pat, _)| pat).collect();

    let mut deserialization = quote! {};
    for (i, (pat, ty)) in params.iter().enumerate() {
        let index = syn::Index::from(i);
        deserialization.extend(quote! {
            let #pat: #ty = serde_json::from_value(args[#index].clone())
                .map_err(|e| format!("Argument {}: {}", #i, e))?;
        });
    }

    input_fn.sig.output = syn::parse_quote!(-> Result<impl serde::Serialize, String>);

    TokenStream::from(quote! {
        #input_fn

        #[doc(hidden)]
        #[allow(non_upper_case_globals)]
        static #handler_ident: () = {
            fn handler(data: serde_json::Value) -> Result<serde_json::Value, String> {
                let args: Vec<serde_json::Value> = serde_json::from_value(data)
                    .map_err(|e| e.to_string())?;

                if args.len() != #param_count {
                    return Err(format!(
                        "Expected {} arguments, got {}",
                        #param_count,
                        args.len()
                    ));
                }

                #deserialization

                let result = #fn_name(#(#param_names),*)?;
                serde_json::to_value(result).map_err(|e| e.to_string())
            }

            crosslic::inventory::submit! {
                crosslic::CommandDescriptor {
                    name: #cmd_name,
                    handler
                }
            }
        };
    })
}