use async_trait::async_trait;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(self.body.clone())
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_slice(&self.body)
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
}
#[cfg_attr(not(feature = "browser"), async_trait)]
#[cfg_attr(feature = "browser", async_trait(?Send))]
pub trait HttpClient: Send + Sync {
async fn request(
&self,
method: &str,
url: &str,
headers: &HashMap<String, String>,
body: Option<&[u8]>,
) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>>;
async fn get(
&self,
url: &str,
headers: &HashMap<String, String>,
) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
self.request("GET", url, headers, None).await
}
async fn post(
&self,
url: &str,
headers: &HashMap<String, String>,
body: &[u8],
) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
self.request("POST", url, headers, Some(body)).await
}
}
#[cfg(feature = "native")]
pub struct NativeHttpClient {
client: reqwest::Client,
}
#[cfg(feature = "native")]
impl NativeHttpClient {
pub fn new() -> Self {
Self {
client: reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.pool_idle_timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to build reqwest client"),
}
}
}
#[cfg(feature = "native")]
impl Default for NativeHttpClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "native")]
#[async_trait]
impl HttpClient for NativeHttpClient {
async fn request(
&self,
method: &str,
url: &str,
headers: &HashMap<String, String>,
body: Option<&[u8]>,
) -> Result<HttpResponse, Box<dyn std::error::Error + Send + Sync>> {
let reqwest_method = method.parse::<reqwest::Method>()?;
let mut builder = self.client.request(reqwest_method, url);
for (key, value) in headers {
builder = builder.header(key.as_str(), value.as_str());
}
if let Some(body_bytes) = body {
builder = builder.body(body_bytes.to_vec());
}
let response = builder.send().await?;
let status = response.status().as_u16();
let mut resp_headers = HashMap::new();
for (key, value) in response.headers() {
if let Ok(v) = value.to_str() {
resp_headers.insert(key.as_str().to_string(), v.to_string());
}
}
let resp_body = response.bytes().await?.to_vec();
Ok(HttpResponse {
status,
headers: resp_headers,
body: resp_body,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_response_text() {
let response = HttpResponse {
status: 200,
headers: HashMap::new(),
body: b"hello world".to_vec(),
};
assert_eq!(response.text().unwrap(), "hello world");
}
#[test]
fn test_http_response_text_invalid_utf8() {
let response = HttpResponse {
status: 200,
headers: HashMap::new(),
body: vec![0xFF, 0xFE],
};
assert!(response.text().is_err());
}
#[test]
fn test_http_response_json() {
let response = HttpResponse {
status: 200,
headers: HashMap::new(),
body: br#"{"key": "value"}"#.to_vec(),
};
let parsed: HashMap<String, String> = response.json().unwrap();
assert_eq!(parsed.get("key").unwrap(), "value");
}
#[test]
fn test_http_response_json_invalid() {
let response = HttpResponse {
status: 200,
headers: HashMap::new(),
body: b"not json".to_vec(),
};
let result: Result<HashMap<String, String>, _> = response.json();
assert!(result.is_err());
}
#[test]
fn test_http_response_is_success() {
for status in [200, 201, 204, 299] {
let response = HttpResponse {
status,
headers: HashMap::new(),
body: vec![],
};
assert!(response.is_success(), "status {status} should be success");
}
}
#[test]
fn test_http_response_is_not_success() {
for status in [100, 301, 400, 404, 500] {
let response = HttpResponse {
status,
headers: HashMap::new(),
body: vec![],
};
assert!(
!response.is_success(),
"status {status} should not be success"
);
}
}
#[test]
fn test_native_http_client_default() {
let _client = NativeHttpClient::default();
}
}