#![allow(clippy::tabs_in_doc_comments)]
#![deny(missing_docs)]
use proc_macro::TokenStream;
use convert_case::{Case, Casing};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
};
#[derive(Debug)]
enum ApiProperty {
Category(String),
Method(String),
Accept(String),
Uri(String),
}
impl Parse for ApiProperty {
fn parse(input: ParseStream) -> Result<Self> {
let name = input.parse::<Ident>()?.to_string();
let value = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
if input.peek(LitStr) {
input.parse::<LitStr>()?.value()
} else {
panic!("expect a `LitStr` here");
}
} else {
panic!("expect a `Token![=]` here")
};
Ok(match name.as_str() {
"category" => ApiProperty::Category(value),
"method" => ApiProperty::Method(value),
"accept" => ApiProperty::Accept(value),
"uri" => ApiProperty::Uri(value),
property => panic!(
"expect one of the [\"category\", \"method\", \"accept\", \"uri\"] but found {property:?}"
),
})
}
}
#[proc_macro_attribute]
pub fn api(_: TokenStream, input: TokenStream) -> TokenStream {
let api_struct = syn::parse_macro_input!(input as ItemStruct);
let api_attrs = api_struct.attrs;
let api_name = api_struct.ident;
let mut api_doc = String::new();
let mut api_method = String::new();
let mut api_accept = String::new();
let mut api_uri = String::new();
api_attrs
.into_iter()
.filter(|attr| attr.path.is_ident("properties"))
.flat_map(|attr| {
attr.parse_args_with(Punctuated::<ApiProperty, Token![,]>::parse_terminated)
.unwrap()
.into_iter()
})
.for_each(|property| match property {
ApiProperty::Category(category) =>
api_doc = format!(
" - <https://docs.github.com/en/rest/{category}/{category}#{}>",
api_name.to_string().to_case(Case::Kebab)
),
ApiProperty::Method(method) => api_method = method,
ApiProperty::Accept(accept) => api_accept = accept,
ApiProperty::Uri(uri) => api_uri = format!("{{}}{uri}"),
});
let api_vis = api_struct.vis;
let api_generics = api_struct.generics;
let mut api_path_params = Vec::new();
let mut api_path_params_tys = Vec::new();
let mut api_payload_ess_params = Vec::new();
let mut api_payload_ess_params_tys = Vec::new();
let mut api_payload_opt_params = Vec::new();
let mut api_payload_opt_params_tys = Vec::new();
{
let Fields::Named(fields) = api_struct.fields else {
panic!("expect a `Fields::Named` here");
};
fields.named.into_iter().for_each(|field| {
if field.attrs.is_empty() {
let Type::Path(path) = field.ty else { panic!("expect a `Path` here"); };
if &path.path.segments[0].ident.to_string() == "Option" {
api_payload_opt_params.push(field.ident);
let PathArguments::AngleBracketed(args) = &path.path.segments[0].arguments else {
panic!("expect a `PathArguments::AngleBracketed` here");
};
let GenericArgument::Type(ty) = &args.args[0] else { panic!("expect a `GenericArgument::Type` here"); };
api_payload_opt_params_tys.push(ty.to_owned());
} else {
panic!("expect an `Option` here");
}
} else {
match field.attrs[0].path.get_ident().unwrap().to_string().as_str() {
"path_param" => {
api_path_params.push(field.ident);
api_path_params_tys.push(field.ty);
},
"payload_ess_param" => {
api_payload_ess_params.push(field.ident);
api_payload_ess_params_tys.push(field.ty);
},
ident => panic!(
"expect one of the [\"path_param\", \"payload_ess_param\"] but found {ident:?}"
),
}
}
});
}
let api_method = quote::format_ident!("{}", api_method.to_case(Case::Pascal));
let get_names = |params: &[Option<Ident>]| {
params
.iter()
.map(|field| {
field.as_ref().map(|field| field.to_string().trim_start_matches("r#").to_owned())
})
.collect::<Vec<_>>()
};
let api_payload_ess_params_names = get_names(&api_payload_ess_params);
let api_payload_opt_params_names = get_names(&api_payload_opt_params);
let api_name_snake_case = quote::format_ident!("{}", api_name.to_string().to_case(Case::Snake));
quote::quote! {
#[doc = #api_doc]
#[derive(Debug, Clone, PartialEq, Eq)]
#api_vis struct #api_name #api_generics {
#(
#[allow(missing_docs)]
#api_vis #api_path_params: #api_path_params_tys,
)*
#(
#[allow(missing_docs)]
#api_vis #api_payload_ess_params: #api_payload_ess_params_tys,
)*
#(
#[allow(missing_docs)]
#api_vis #api_payload_opt_params: Option<#api_payload_opt_params_tys>,
)*
}
impl #api_generics #api_name #api_generics {
#[doc = concat!(
"Build a [`",
stringify!(#api_name),
"`] instance."
)]
#api_vis fn new(
#(#api_path_params: #api_path_params_tys,)*
#(#api_payload_ess_params: #api_payload_ess_params_tys,)*
) -> Self {
Self {
#(#api_path_params,)*
#(#api_payload_ess_params,)*
#(#api_payload_opt_params: None,)*
}
}
#(
#[doc = concat!(
"Set a new [`",
stringify!(#api_path_params),
"`](",
stringify!(#api_name),
"#structfield.",
stringify!(#api_path_params),
")."
)]
#api_vis fn #api_path_params(
mut self,
#api_path_params: #api_path_params_tys
) -> Self {
self.#api_path_params = #api_path_params;
self
}
)*
#(
#[doc = concat!(
"Set a new [`",
stringify!(#api_payload_ess_params),
"`](",
stringify!(#api_name),
"#structfield.",
stringify!(#api_payload_ess_params),
")."
)]
#api_vis fn #api_payload_ess_params(
mut self,
#api_payload_ess_params: #api_payload_ess_params_tys
) -> Self {
self.#api_payload_ess_params = #api_payload_ess_params;
self
}
)*
#(
#[doc = concat!(
"Set a new [`",
stringify!(#api_payload_opt_params),
"`](",
stringify!(#api_name),
"#structfield.",
stringify!(#api_payload_opt_params),
")."
)]
#api_vis fn #api_payload_opt_params(
mut self,
#api_payload_opt_params: #api_payload_opt_params_tys
) -> Self {
self.#api_payload_opt_params = Some(#api_payload_opt_params);
self
}
)*
}
impl #api_generics Api for #api_name #api_generics {
const ACCEPT: &'static str = #api_accept;
fn api(&self) -> String {
format!(
#api_uri,
Self::BASE_URI,
#(self.#api_path_params,)*
)
}
}
impl #api_generics ApiExt for #api_name #api_generics {
const METHOD: Method = Method::#api_method;
fn payload_params(&self) -> Vec<(&'static str, String)> {
let mut payload_params = vec![
#((
#api_payload_ess_params_names,
self.#api_payload_ess_params.to_string()
),)*
];
#(
if let Some(#api_payload_opt_params) = self.#api_payload_opt_params {
payload_params.push((
#api_payload_opt_params_names,
#api_payload_opt_params.to_string()
));
}
)*
payload_params
}
}
#[doc = concat!(
"Build a [`",
stringify!(#api_name),
"`] instance."
)]
#api_vis fn #api_name_snake_case #api_generics(
#(#api_path_params: #api_path_params_tys,)*
#(#api_payload_ess_params: #api_payload_ess_params_tys,)*
) -> #api_name #api_generics {
#api_name::new(
#(#api_path_params,)*
#(#api_payload_ess_params,)*
)
}
}
.into()
}