1#![allow(clippy::tabs_in_doc_comments)]
4#![deny(missing_docs)]
5
6use proc_macro::TokenStream;
8use convert_case::{Case, Casing};
10use syn::{
11 parse::{Parse, ParseStream},
12 punctuated::Punctuated,
13 *,
14};
15
16#[derive(Debug)]
17enum ApiProperty {
18 Category(String),
19 Method(String),
20 Accept(String),
21 Uri(String),
22}
23impl Parse for ApiProperty {
24 fn parse(input: ParseStream) -> Result<Self> {
25 let name = input.parse::<Ident>()?.to_string();
26 let value = if input.peek(Token![=]) {
27 input.parse::<Token![=]>()?;
28
29 if input.peek(LitStr) {
30 input.parse::<LitStr>()?.value()
31 } else {
32 panic!("expect a `LitStr` here");
33 }
34 } else {
35 panic!("expect a `Token![=]` here")
36 };
37
38 Ok(match name.as_str() {
39 "category" => ApiProperty::Category(value),
40 "method" => ApiProperty::Method(value),
41 "accept" => ApiProperty::Accept(value),
42 "uri" => ApiProperty::Uri(value),
43 property => panic!(
44 "expect one of the [\"category\", \"method\", \"accept\", \"uri\"] but found {property:?}"
45 ),
46 })
47 }
48}
49
50#[proc_macro_attribute]
110pub fn api(_: TokenStream, input: TokenStream) -> TokenStream {
111 let api_struct = syn::parse_macro_input!(input as ItemStruct);
112
113 let api_attrs = api_struct.attrs;
119
120 let api_name = api_struct.ident;
124 let mut api_doc = String::new();
125 let mut api_method = String::new();
126 let mut api_accept = String::new();
127 let mut api_uri = String::new();
128
129 api_attrs
130 .into_iter()
131 .filter(|attr| attr.path.is_ident("properties"))
132 .flat_map(|attr| {
133 attr.parse_args_with(Punctuated::<ApiProperty, Token![,]>::parse_terminated)
134 .unwrap()
135 .into_iter()
136 })
137 .for_each(|property| match property {
138 ApiProperty::Category(category) =>
139 api_doc = format!(
140 " - <https://docs.github.com/en/rest/{category}/{category}#{}>",
141 api_name.to_string().to_case(Case::Kebab)
142 ),
143 ApiProperty::Method(method) => api_method = method,
144 ApiProperty::Accept(accept) => api_accept = accept,
145 ApiProperty::Uri(uri) => api_uri = format!("{{}}{uri}"),
146 });
147
148 let api_vis = api_struct.vis;
149 let api_generics = api_struct.generics;
150 let mut api_path_params = Vec::new();
151 let mut api_path_params_tys = Vec::new();
152 let mut api_payload_ess_params = Vec::new();
153 let mut api_payload_ess_params_tys = Vec::new();
154 let mut api_payload_opt_params = Vec::new();
155 let mut api_payload_opt_params_tys = Vec::new();
156
157 {
158 let Fields::Named(fields) = api_struct.fields else {
159 panic!("expect a `Fields::Named` here");
160 };
161
162 fields.named.into_iter().for_each(|field| {
163 if field.attrs.is_empty() {
164 let Type::Path(path) = field.ty else { panic!("expect a `Path` here"); };
165
166 if &path.path.segments[0].ident.to_string() == "Option" {
167 api_payload_opt_params.push(field.ident);
168
169 let PathArguments::AngleBracketed(args) = &path.path.segments[0].arguments else {
170 panic!("expect a `PathArguments::AngleBracketed` here");
171 };
172 let GenericArgument::Type(ty) = &args.args[0] else { panic!("expect a `GenericArgument::Type` here"); };
173
174 api_payload_opt_params_tys.push(ty.to_owned());
175 } else {
176 panic!("expect an `Option` here");
177 }
178 } else {
179 match field.attrs[0].path.get_ident().unwrap().to_string().as_str() {
180 "path_param" => {
181 api_path_params.push(field.ident);
182 api_path_params_tys.push(field.ty);
183 },
184 "payload_ess_param" => {
185 api_payload_ess_params.push(field.ident);
186 api_payload_ess_params_tys.push(field.ty);
187 },
188 ident => panic!(
189 "expect one of the [\"path_param\", \"payload_ess_param\"] but found {ident:?}"
190 ),
191 }
192 }
193 });
194 }
195
196 let api_method = quote::format_ident!("{}", api_method.to_case(Case::Pascal));
197 let get_names = |params: &[Option<Ident>]| {
198 params
199 .iter()
200 .map(|field| {
201 field.as_ref().map(|field| field.to_string().trim_start_matches("r#").to_owned())
202 })
203 .collect::<Vec<_>>()
204 };
205 let api_payload_ess_params_names = get_names(&api_payload_ess_params);
206 let api_payload_opt_params_names = get_names(&api_payload_opt_params);
207 let api_name_snake_case = quote::format_ident!("{}", api_name.to_string().to_case(Case::Snake));
208
209 quote::quote! {
210 #[doc = #api_doc]
212 #[derive(Debug, Clone, PartialEq, Eq)]
213 #api_vis struct #api_name #api_generics {
214 #(
215 #[allow(missing_docs)]
216 #api_vis #api_path_params: #api_path_params_tys,
217 )*
218 #(
219 #[allow(missing_docs)]
220 #api_vis #api_payload_ess_params: #api_payload_ess_params_tys,
221 )*
222 #(
223 #[allow(missing_docs)]
224 #api_vis #api_payload_opt_params: Option<#api_payload_opt_params_tys>,
225 )*
226 }
227 impl #api_generics #api_name #api_generics {
228 #[doc = concat!(
229 "Build a [`",
230 stringify!(#api_name),
231 "`] instance."
232 )]
233 #api_vis fn new(
234 #(#api_path_params: #api_path_params_tys,)*
235 #(#api_payload_ess_params: #api_payload_ess_params_tys,)*
236 ) -> Self {
237 Self {
238 #(#api_path_params,)*
239 #(#api_payload_ess_params,)*
240 #(#api_payload_opt_params: None,)*
241 }
242 }
243
244 #(
245 #[doc = concat!(
246 "Set a new [`",
247 stringify!(#api_path_params),
248 "`](",
249 stringify!(#api_name),
250 "#structfield.",
251 stringify!(#api_path_params),
252 ")."
253 )]
254 #api_vis fn #api_path_params(
255 mut self,
256 #api_path_params: #api_path_params_tys
257 ) -> Self {
258 self.#api_path_params = #api_path_params;
259
260 self
261 }
262 )*
263 #(
264 #[doc = concat!(
265 "Set a new [`",
266 stringify!(#api_payload_ess_params),
267 "`](",
268 stringify!(#api_name),
269 "#structfield.",
270 stringify!(#api_payload_ess_params),
271 ")."
272 )]
273 #api_vis fn #api_payload_ess_params(
274 mut self,
275 #api_payload_ess_params: #api_payload_ess_params_tys
276 ) -> Self {
277 self.#api_payload_ess_params = #api_payload_ess_params;
278
279 self
280 }
281 )*
282 #(
283 #[doc = concat!(
284 "Set a new [`",
285 stringify!(#api_payload_opt_params),
286 "`](",
287 stringify!(#api_name),
288 "#structfield.",
289 stringify!(#api_payload_opt_params),
290 ")."
291 )]
292 #api_vis fn #api_payload_opt_params(
293 mut self,
294 #api_payload_opt_params: #api_payload_opt_params_tys
295 ) -> Self {
296 self.#api_payload_opt_params = Some(#api_payload_opt_params);
297
298 self
299 }
300 )*
301 }
302 impl #api_generics Api for #api_name #api_generics {
303 const ACCEPT: &'static str = #api_accept;
304
305 fn api(&self) -> String {
306 format!(
307 #api_uri,
308 Self::BASE_URI,
309 #(self.#api_path_params,)*
310 )
311 }
312 }
313 impl #api_generics ApiExt for #api_name #api_generics {
314 const METHOD: Method = Method::#api_method;
315
316 fn payload_params(&self) -> Vec<(&'static str, String)> {
317 let mut payload_params = vec![
318 #((
319 #api_payload_ess_params_names,
320 self.#api_payload_ess_params.to_string()
321 ),)*
322 ];
323
324 #(
325 if let Some(#api_payload_opt_params) = self.#api_payload_opt_params {
326 payload_params.push((
327 #api_payload_opt_params_names,
328 #api_payload_opt_params.to_string()
329 ));
330 }
331 )*
332
333 payload_params
334 }
335 }
336 #[doc = concat!(
337 "Build a [`",
338 stringify!(#api_name),
339 "`] instance."
340 )]
341 #api_vis fn #api_name_snake_case #api_generics(
342 #(#api_path_params: #api_path_params_tys,)*
343 #(#api_payload_ess_params: #api_payload_ess_params_tys,)*
344 ) -> #api_name #api_generics {
345 #api_name::new(
346 #(#api_path_params,)*
347 #(#api_payload_ess_params,)*
348 )
349 }
350 }
351 .into()
352}