pub use responses::RawResponse;
use std::{collections::HashMap, time::Duration};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
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",
HttpMethod::Head => "HEAD",
HttpMethod::Options => "OPTIONS",
}
}
}
impl std::fmt::Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub enum RequestData {
Json(serde_json::Value),
Text(String),
Binary(Vec<u8>),
Form(std::collections::HashMap<String, String>),
}
impl From<String> for RequestData {
fn from(value: String) -> Self {
RequestData::Text(value)
}
}
impl From<&str> for RequestData {
fn from(value: &str) -> Self {
RequestData::Text(value.to_string())
}
}
impl From<serde_json::Value> for RequestData {
fn from(value: serde_json::Value) -> Self {
RequestData::Json(value)
}
}
impl From<Vec<u8>> for RequestData {
fn from(value: Vec<u8>) -> Self {
RequestData::Binary(value)
}
}
impl From<std::collections::HashMap<String, String>> for RequestData {
fn from(value: std::collections::HashMap<String, String>) -> Self {
RequestData::Form(value)
}
}
pub use responses::{ApiResponseTrait, BaseResponse, ErrorInfo, Response, ResponseFormat};
#[derive(Debug, Clone)]
pub struct ApiRequest<R> {
pub(crate) method: HttpMethod,
pub(crate) url: String,
pub(crate) headers: HashMap<String, String>,
pub(crate) query: HashMap<String, String>,
pub(crate) body: Option<RequestData>,
pub(crate) file: Option<Vec<u8>>,
pub(crate) timeout: Option<Duration>,
pub(crate) _phantom: std::marker::PhantomData<R>,
}
impl<R> ApiRequest<R> {
pub fn get(url: impl Into<String>) -> Self {
Self {
method: HttpMethod::Get,
url: url.into(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
pub fn post(url: impl Into<String>) -> Self {
Self {
method: HttpMethod::Post,
url: url.into(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
pub fn put(url: impl Into<String>) -> Self {
Self {
method: HttpMethod::Put,
url: url.into(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
pub fn patch(url: impl Into<String>) -> Self {
Self {
method: HttpMethod::Patch,
url: url.into(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
pub fn delete(url: impl Into<String>) -> Self {
Self {
method: HttpMethod::Delete,
url: url.into(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.headers.insert(key.into(), value.into());
self
}
pub fn query<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.query.insert(key.into(), value.into());
self
}
pub fn query_opt<K, V>(mut self, key: K, value: Option<V>) -> Self
where
K: Into<String>,
V: Into<String>,
{
if let Some(v) = value {
self.query.insert(key.into(), v.into());
}
self
}
pub fn body(mut self, body: impl Into<RequestData>) -> Self {
self.body = Some(body.into());
self
}
pub fn file_content(mut self, file: Vec<u8>) -> Self {
self.file = Some(file);
self
}
pub fn json_body<T>(mut self, body: &T) -> Self
where
T: serde::Serialize,
{
match serde_json::to_value(body) {
Ok(json_value) => self.body = Some(RequestData::Json(json_value)),
Err(e) => {
tracing::warn!(error = %e, "json_body 序列化失败");
self.body = Some(RequestData::Json(serde_json::Value::Null));
}
}
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = Some(duration);
self
}
pub fn build_url(&self) -> String {
if self.query.is_empty() {
self.url.clone()
} else {
let query_string = self
.query
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", self.url, query_string)
}
}
pub fn method(&self) -> &HttpMethod {
&self.method
}
pub fn api_path(&self) -> &str {
if let Some(start) = self.url.find(crate::constants::API_PATH_PREFIX) {
&self.url[start..]
} else {
&self.url
}
}
pub fn supported_access_token_types(&self) -> Vec<crate::constants::AccessTokenType> {
vec![
crate::constants::AccessTokenType::User,
crate::constants::AccessTokenType::Tenant,
]
}
pub fn to_bytes(&self) -> Vec<u8> {
match &self.body {
Some(RequestData::Json(data)) => serde_json::to_vec(data).unwrap_or_default(),
Some(RequestData::Binary(data)) => data.clone(),
Some(RequestData::Form(data)) => data
.iter()
.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
.collect::<Vec<_>>()
.join("&")
.into_bytes(),
Some(RequestData::Text(text)) => text.clone().into_bytes(),
None => vec![],
}
}
pub fn headers_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.headers
}
pub fn query_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.query
}
pub fn file(&self) -> Vec<u8> {
self.file.clone().unwrap_or_default()
}
pub fn request_option(mut self, option: crate::req_option::RequestOption) -> Self {
for (key, value) in option.header {
self = self.header(key, value);
}
self
}
pub fn query_param<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.query.insert(key.into(), value.into());
self
}
pub fn query_params<I, K, V>(mut self, params: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
for (key, value) in params {
self.query.insert(key.into(), value.into());
}
self
}
}
impl<R> Default for ApiRequest<R> {
fn default() -> Self {
Self {
method: HttpMethod::Get,
url: String::default(),
headers: HashMap::new(),
query: HashMap::new(),
body: None,
file: None,
timeout: None,
_phantom: std::marker::PhantomData,
}
}
}
pub type ApiResponse<R> = Response<R>;
pub mod prelude;
pub mod responses;
pub mod traits;
pub use traits::{AsyncApiClient, SyncApiClient};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_patch_method() {
let request: ApiRequest<()> = ApiRequest::patch("https://example.com/api/resource");
assert_eq!(request.method, HttpMethod::Patch);
assert_eq!(request.url, "https://example.com/api/resource");
assert_eq!(request.method.as_str(), "PATCH");
println!("✅ Patch method test passed!");
}
#[test]
fn test_all_http_methods() {
let get_req: ApiRequest<()> = ApiRequest::get("https://example.com/api");
let post_req: ApiRequest<()> = ApiRequest::post("https://example.com/api");
let put_req: ApiRequest<()> = ApiRequest::put("https://example.com/api");
let patch_req: ApiRequest<()> = ApiRequest::patch("https://example.com/api");
let delete_req: ApiRequest<()> = ApiRequest::delete("https://example.com/api");
assert_eq!(get_req.method, HttpMethod::Get);
assert_eq!(post_req.method, HttpMethod::Post);
assert_eq!(put_req.method, HttpMethod::Put);
assert_eq!(patch_req.method, HttpMethod::Patch);
assert_eq!(delete_req.method, HttpMethod::Delete);
assert_eq!(get_req.method.as_str(), "GET");
assert_eq!(post_req.method.as_str(), "POST");
assert_eq!(put_req.method.as_str(), "PUT");
assert_eq!(patch_req.method.as_str(), "PATCH");
assert_eq!(delete_req.method.as_str(), "DELETE");
println!("✅ All HTTP methods test passed!");
}
}