1use proc_macro::TokenStream;
2
3use quote::quote;
4use syn::{
5 Attribute, Expr, FnArg, ImplItem, ImplItemFn, ItemImpl, Meta, Pat, Type, parse_macro_input,
6};
7
8#[derive(Default)]
9struct ApiAction {
10 action: Option<String>,
11 extract: Option<String>,
12 response_type: Option<Type>,
13 renames: Vec<(String, String)>,
14 discard: bool,
15}
16
17fn take_api_attr(attrs: &mut Vec<Attribute>) -> Option<Attribute> {
18 let idx = attrs.iter().position(|a| a.path().is_ident("api"))?;
19 Some(attrs.remove(idx))
20}
21
22fn parse_string_meta(nv: &syn::MetaNameValue) -> Option<String> {
23 let Expr::Lit(lit) = &nv.value else {
24 return None;
25 };
26 let syn::Lit::Str(s) = &lit.lit else {
27 return None;
28 };
29 Some(s.value())
30}
31
32fn parse_type_meta(nv: &syn::MetaNameValue) -> Option<Type> {
33 let Expr::Path(expr_path) = &nv.value else {
34 return None;
35 };
36 Some(Type::Path(syn::TypePath {
37 qself: None,
38 path: expr_path.path.clone(),
39 }))
40}
41
42fn parse_bool_meta(nv: &syn::MetaNameValue) -> Option<bool> {
43 let Expr::Lit(lit) = &nv.value else {
44 return None;
45 };
46 let syn::Lit::Bool(b) = &lit.lit else {
47 return None;
48 };
49 Some(b.value)
50}
51
52fn parse_map_list(list: &syn::MetaList) -> Vec<(String, String)> {
53 let Ok(inner) = list
54 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
55 else {
56 return Vec::new();
57 };
58
59 let mut renames = Vec::new();
60 for m in inner {
61 let Meta::NameValue(nv) = m else {
62 continue;
63 };
64 let Some(param_name) = nv.path.get_ident().map(|i| i.to_string()) else {
65 continue;
66 };
67 if let Some(value) = parse_string_meta(&nv) {
68 renames.push((param_name, value));
69 }
70 }
71 renames
72}
73
74fn parse_api_meta(meta: Meta, action: &mut ApiAction) {
75 match meta {
76 Meta::NameValue(nv) if nv.path.is_ident("action") => {
77 action.action = parse_string_meta(&nv);
78 }
79 Meta::NameValue(nv) if nv.path.is_ident("extract") => {
80 action.extract = parse_string_meta(&nv);
81 }
82 Meta::NameValue(nv) if nv.path.is_ident("response") => {
83 action.response_type = parse_type_meta(&nv);
84 }
85 Meta::NameValue(nv) if nv.path.is_ident("discard") => {
86 action.discard = parse_bool_meta(&nv).unwrap_or(false);
87 }
88 Meta::List(list) if list.path.is_ident("map") => {
89 action.renames = parse_map_list(&list);
90 }
91 _ => {}
92 }
93}
94
95fn take_api_action(attrs: &mut Vec<Attribute>) -> ApiAction {
96 let attr = match take_api_attr(attrs) {
97 Some(a) => a,
98 None => return ApiAction::default(),
99 };
100
101 let meta_list = match attr.meta.require_list() {
102 Ok(list) => list,
103 Err(_) => return ApiAction::default(),
104 };
105
106 let nested = match meta_list
107 .parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated)
108 {
109 Ok(n) => n,
110 Err(_) => return ApiAction::default(),
111 };
112
113 let mut action = ApiAction::default();
114 for meta in nested {
115 parse_api_meta(meta, &mut action);
116 }
117
118 action
119}
120
121fn generate_method_body(method: &ImplItemFn, action: &ApiAction) -> proc_macro2::TokenStream {
122 let action_name = action
123 .action
124 .clone()
125 .unwrap_or_else(|| method.sig.ident.to_string());
126 let action_lit = syn::LitStr::new(&action_name, proc_macro2::Span::call_site());
127
128 let json_entries: Vec<proc_macro2::TokenStream> = method
129 .sig
130 .inputs
131 .iter()
132 .filter_map(|arg| {
133 if let FnArg::Typed(pat_type) = arg
134 && let Pat::Ident(pat_ident) = &*pat_type.pat
135 {
136 let param_name = pat_ident.ident.to_string();
137 let json_key = action
138 .renames
139 .iter()
140 .find(|(p, _)| p == ¶m_name)
141 .map(|(_, k)| k.clone())
142 .unwrap_or_else(|| param_name.clone());
143
144 let param_ident = &pat_ident.ident;
145 let json_key_lit = syn::LitStr::new(&json_key, proc_macro2::Span::call_site());
146
147 return Some(quote! {
148 #json_key_lit: #param_ident,
149 });
150 }
151 None
152 })
153 .collect();
154
155 if action.discard {
156 quote!({
157 let params = ::serde_json::json!({
158 #(#json_entries)*
159 });
160 let _: ::serde_json::Value = self.send_and_parse(#action_lit, params).await?;
161 Ok(())
162 })
163 } else if let Some(ref extract_field) = action.extract {
164 let response_type = action
165 .response_type
166 .as_ref()
167 .expect("#[api(extract = \"...\")] 需要同时指定 #[api(response = Type)] 以确定中间响应类型");
168 let extract_ident = syn::Ident::new(extract_field, proc_macro2::Span::call_site());
169
170 quote!({
171 let params = ::serde_json::json!({
172 #(#json_entries)*
173 });
174 let response: #response_type = self.send_and_parse(#action_lit, params).await?;
175 Ok(response.#extract_ident)
176 })
177 } else {
178 quote!({
179 let params = ::serde_json::json!({
180 #(#json_entries)*
181 });
182 self.send_and_parse(#action_lit, params).await
183 })
184 }
185}
186
187#[proc_macro_attribute]
188pub fn api_sender(_attr: TokenStream, item: TokenStream) -> TokenStream {
189 let mut impl_block = parse_macro_input!(item as ItemImpl);
190
191 let mut new_items: Vec<ImplItem> = Vec::with_capacity(impl_block.items.len());
192
193 for item in std::mem::take(&mut impl_block.items) {
194 if let ImplItem::Fn(mut method) = item {
195 let api_action = take_api_action(&mut method.attrs);
196 let body = generate_method_body(&method, &api_action);
197 method.block = syn::parse2(body).expect("生成方法体失败");
198 new_items.push(ImplItem::Fn(method));
199 } else {
200 new_items.push(item);
201 }
202 }
203
204 impl_block.items = new_items;
205
206 quote!(#impl_block).into()
207}