use syn::{
braced, parenthesized,
parse::{Parse, ParseStream, Result},
punctuated::Punctuated,
Ident, LitInt, LitStr, Token, Type,
};
#[derive(Debug, Clone)]
#[allow(clippy::upper_case_acronyms)]
pub enum HttpMethod {
GET,
POST,
PUT,
DELETE,
PATCH,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::GET => "get",
HttpMethod::POST => "post",
HttpMethod::PUT => "put",
HttpMethod::DELETE => "delete",
HttpMethod::PATCH => "patch",
}
}
}
impl Parse for HttpMethod {
fn parse(input: ParseStream) -> Result<Self> {
let ident: Ident = input.parse()?;
match ident.to_string().to_uppercase().as_str() {
"GET" => Ok(HttpMethod::GET),
"POST" => Ok(HttpMethod::POST),
"PUT" => Ok(HttpMethod::PUT),
"DELETE" => Ok(HttpMethod::DELETE),
"PATCH" => Ok(HttpMethod::PATCH),
_ => Err(syn::Error::new(
ident.span(),
format!("Unsupported HTTP method: {}", ident),
)),
}
}
}
#[derive(Debug, Clone)]
pub enum AuthStrategy {
Bearer,
Basic,
ApiKey(String),
}
impl Parse for AuthStrategy {
fn parse(input: ParseStream) -> Result<Self> {
let ident: Ident = input.parse()?;
match ident.to_string().as_str() {
"Bearer" => Ok(AuthStrategy::Bearer),
"Basic" => Ok(AuthStrategy::Basic),
"ApiKey" => {
let content;
parenthesized!(content in input);
let header: LitStr = content.parse()?;
Ok(AuthStrategy::ApiKey(header.value()))
}
_ => Err(syn::Error::new(
ident.span(),
format!(
"Unsupported auth strategy: {}. Expected Bearer, Basic, or ApiKey(\"header\")",
ident
),
)),
}
}
}
pub struct ApiClientInput {
pub struct_name: Ident,
pub auth: Option<AuthStrategy>,
pub global_retries: Option<u32>,
pub endpoints: Vec<EndpointDef>,
}
pub struct EndpointDef {
pub method: HttpMethod,
pub res: Option<Type>,
pub path: Option<LitStr>,
pub fn_name: Option<Ident>,
pub req: Option<Type>,
pub headers: Option<Type>,
pub query_params: Option<Type>,
pub path_params: Option<Type>,
pub retries: Option<u32>,
}
impl Parse for ApiClientInput {
fn parse(input: ParseStream) -> Result<Self> {
let struct_name: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let mut auth = None;
let mut global_retries = None;
while input.peek(Ident) && input.peek2(Token![:]) {
let fork = input.fork();
let ident: Ident = fork.parse()?;
match ident.to_string().as_str() {
"auth" => {
let _: Ident = input.parse()?;
input.parse::<Token![:]>()?;
auth = Some(input.parse::<AuthStrategy>()?);
input.parse::<Token![,]>()?;
}
"retries" => {
let _: Ident = input.parse()?;
input.parse::<Token![:]>()?;
let lit: LitInt = input.parse()?;
global_retries = Some(lit.base10_parse::<u32>()?);
input.parse::<Token![,]>()?;
}
_ => break,
}
}
let content;
braced!(content in input);
let items: Punctuated<EndpointDef, Token![,]> =
content.parse_terminated(EndpointDef::parse, Token![,])?;
Ok(Self {
struct_name,
auth,
global_retries,
endpoints: items.into_iter().collect(),
})
}
}
impl Parse for EndpointDef {
fn parse(input: ParseStream) -> Result<Self> {
let content;
braced!(content in input);
let mut path = None;
let mut method = None;
let mut fn_name = None;
let mut req = None;
let mut res = None;
let mut headers = None;
let mut query_params = None;
let mut path_params = None;
let mut retries = None;
while !content.is_empty() {
let field: Ident = content.parse()?;
content.parse::<Token![:]>()?;
match field.to_string().as_str() {
"path" => path = Some(content.parse()?),
"method" => method = Some(content.parse()?),
"fn_name" => fn_name = Some(content.parse()?),
"req" => req = Some(content.parse()?),
"res" => res = Some(content.parse()?),
"headers" => headers = Some(content.parse()?),
"query_params" => query_params = Some(content.parse()?),
"path_params" => path_params = Some(content.parse()?),
"retries" => {
let lit: LitInt = content.parse()?;
retries = Some(lit.base10_parse::<u32>()?);
}
_ => return Err(syn::Error::new(field.span(), "unexpected field")),
}
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
Ok(EndpointDef {
path,
method: method.ok_or_else(|| syn::Error::new(content.span(), "missing `method`"))?,
fn_name,
req,
res,
headers,
query_params,
path_params,
retries,
})
}
}