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}