Skip to main content

onebot_api_macros/
lib.rs

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 == &param_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}