use darling::FromMeta;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[derive(Debug, FromMeta)]
struct EndpointOpts {
name: String,
#[darling(default = "EndpointOpts::default_method")]
method: String,
#[darling(default = "EndpointOpts::default_path")]
path: String,
#[darling(default = "EndpointOpts::default_host")]
host: String,
#[darling(default)]
resp: Option<syn::Path>,
}
impl EndpointOpts {
fn default_method() -> String {
"GET".to_string()
}
fn default_path() -> String {
"/w1/api/index.php".to_string()
}
fn default_host() -> String {
"apphis.longhuvip.com".to_string()
}
}
#[proc_macro_derive(ApiEndpoint, attributes(endpoint, serde))]
pub fn api_endpoint_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let endpoint_opts = match input
.attrs
.iter()
.find(|attr| attr.path().is_ident("endpoint"))
.map(|attr| EndpointOpts::from_meta(&attr.meta))
.transpose()
{
Ok(Some(opts)) => opts,
Ok(None) => EndpointOpts {
name: String::new(),
method: EndpointOpts::default_method(),
path: EndpointOpts::default_path(),
host: EndpointOpts::default_host(),
resp: None,
},
Err(e) => return TokenStream::from(e.write_errors()),
};
let actual_response_type = match &endpoint_opts.resp {
Some(ty) => quote! { #ty },
None => quote! { serde_json::Value },
};
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("ApiEndpoint only supports structs with named fields"),
},
_ => panic!("ApiEndpoint only supports structs"),
};
let query_params = fields.iter().map(|field| {
let field_name = field.ident.as_ref().unwrap();
let mut rename_value = None;
for attr in &field.attrs {
if attr.path().is_ident("serde") {
let _ = attr.parse_nested_meta(|meta| {
if let Some(ident) = meta.path.get_ident() {
if ident == "rename" {
rename_value = Some(meta.value()?.parse::<syn::LitStr>()?.value());
}
}
Ok(())
});
}
}
let param_name = rename_value.unwrap_or_else(|| field_name.to_string());
quote! {
(#param_name, self.#field_name.to_string())
}
});
let endpoint_name = &endpoint_opts.name;
let method = &endpoint_opts.method;
let path = &endpoint_opts.path;
let host = &endpoint_opts.host;
let output = if endpoint_opts.resp.is_some() {
quote! {
impl #name {
pub async fn execute(&self) -> Result<#actual_response_type, crate::error::ApiError> {
self.execute_with_host(None).await
}
pub async fn execute_with_host(&self, host: Option<String>) -> Result<#actual_response_type, crate::error::ApiError> {
use reqwest::Client;
use serde_json::Value;
let method = match reqwest::Method::from_bytes(#method.as_bytes()) {
Ok(m) => m,
Err(e) => return Err(crate::error::ApiError::InvalidMethod(e.to_string())),
};
let query_params = vec![
#(#query_params),*
];
let host = host.unwrap_or_else(|| #host.to_string());
let response = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap()
.request(method, format!("https://{}{}", host, #path))
.header("User-Agent", "lhb/5.18.5 (com.kaipanla.www; build:2; iOS 18.3.0) Alamofire/4.9.1")
.header("Content-Type", "application/x-www-form-urlencoded; application/x-www-form-urlencoded; charset=utf-8")
.header("Accept-Language", "zh-Hans-CN;q=1.0")
.query(&query_params)
.send()
.await
.map_err(|e| crate::error::ApiError::from(e))?
.error_for_status()
.map_err(|e| crate::error::ApiError::RequestFailed(e.status().map_or(500, |s| s.as_u16())))?;
let json_value: Value = response.json().await.map_err(|e| crate::error::ApiError::from(e))?;
Ok(<#actual_response_type>::from(json_value))
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", #endpoint_name)
}
}
}
} else {
quote! {
impl #name {
pub async fn execute(&self) -> Result<#actual_response_type, crate::error::ApiError> {
self.execute_with_host(None).await
}
pub async fn execute_with_host(&self, host: Option<String>) -> Result<#actual_response_type, crate::error::ApiError> {
use reqwest::Client;
let method = match reqwest::Method::from_bytes(#method.as_bytes()) {
Ok(m) => m,
Err(e) => return Err(crate::error::ApiError::InvalidMethod(e.to_string())),
};
let query_params = vec![
#(#query_params),*
];
let host = host.unwrap_or_else(|| #host.to_string());
let response = Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap()
.request(method, format!("https://{}{}", host, #path))
.header("User-Agent", "lhb/5.18.5 (com.kaipanla.www; build:2; iOS 18.3.0) Alamofire/4.9.1")
.header("Content-Type", "application/x-www-form-urlencoded; application/x-www-form-urlencoded; charset=utf-8")
.header("Accept-Language", "zh-Hans-CN;q=1.0")
.query(&query_params)
.send()
.await
.map_err(crate::error::ApiError::from)?
.error_for_status()
.map_err(|e| crate::error::ApiError::RequestFailed(e.status().map_or(500, |s| s.as_u16())))?;
response.json::<#actual_response_type>()
.await
.map_err(ApiError::from)
}
}
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", #endpoint_name)
}
}
}
};
output.into()
}