crosslic_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{AttributeArgs, FnArg, ItemFn, Lit, Meta, NestedMeta, Pat, Type, parse_macro_input};
4
5#[proc_macro_attribute]
6pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream {
7    let args = parse_macro_input!(attr as AttributeArgs);
8    let mut name_override = None;
9    for arg in args {
10        if let NestedMeta::Meta(Meta::NameValue(nv)) = arg {
11            if nv.path.is_ident("name") {
12                if let Lit::Str(litstr) = nv.lit {
13                    name_override = Some(litstr.value());
14                }
15            }
16        }
17    }
18
19    let mut input_fn = parse_macro_input!(item as ItemFn);
20    let fn_name = input_fn.sig.ident.clone();
21    let handler_ident = format_ident!("__crosslic_handler_{}", fn_name);
22    let cmd_name = name_override.unwrap_or_else(|| fn_name.to_string());
23
24    let params: Vec<(Box<Pat>, Box<Type>)> = input_fn
25        .sig
26        .inputs
27        .iter()
28        .filter_map(|arg| {
29            if let FnArg::Typed(pat_type) = arg {
30                Some((pat_type.pat.clone(), pat_type.ty.clone()))
31            } else {
32                None
33            }
34        })
35        .collect();
36
37    let param_count = params.len();
38    let param_names: Vec<_> = params.iter().map(|(pat, _)| pat).collect();
39
40    let mut deserialization = quote! {};
41    for (i, (pat, ty)) in params.iter().enumerate() {
42        let index = syn::Index::from(i);
43        deserialization.extend(quote! {
44            let #pat: #ty = serde_json::from_value(args[#index].clone())
45                .map_err(|e| format!("Argument {}: {}", #i, e))?;
46        });
47    }
48
49    input_fn.sig.output = syn::parse_quote!(-> Result<impl serde::Serialize, String>);
50
51    TokenStream::from(quote! {
52        #input_fn
53
54        #[doc(hidden)]
55        #[allow(non_upper_case_globals)]
56        static #handler_ident: () = {
57            fn handler(data: serde_json::Value) -> Result<serde_json::Value, String> {
58                let args: Vec<serde_json::Value> = serde_json::from_value(data)
59                    .map_err(|e| e.to_string())?;
60
61                if args.len() != #param_count {
62                    return Err(format!(
63                        "Expected {} arguments, got {}",
64                        #param_count,
65                        args.len()
66                    ));
67                }
68
69                #deserialization
70
71                let result = #fn_name(#(#param_names),*)?;
72                serde_json::to_value(result).map_err(|e| e.to_string())
73            }
74
75            crosslic::inventory::submit! {
76                crosslic::CommandDescriptor {
77                    name: #cmd_name,
78                    handler
79                }
80            }
81        };
82    })
83}