use super::types::*;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum HttpError {
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Middleware error: {0}")]
Middleware(#[from] reqwest_middleware::Error),
#[error("Failed to serialize request: {0}")]
Serialization(String),
#[error("Failed to deserialize response: {0}")]
Deserialization(String),
#[error("HTTP error {status}: {message}")]
Http { status: u16, message: String, body: Option<String> },
#[error("Authentication error: {0}")]
Auth(String),
#[error("Request timeout")]
Timeout,
#[error("Configuration error: {0}")]
Config(String),
#[error("{0}")]
Other(String),
}
impl HttpError {
pub fn from_status(
status: u16,
message: impl Into<String>,
body: Option<String>,
) -> Self {
Self::Http {
status,
message: message.into(),
body,
}
}
pub fn serialization_error(error: impl std::fmt::Display) -> Self {
Self::Serialization(error.to_string())
}
pub fn deserialization_error(error: impl std::fmt::Display) -> Self {
Self::Deserialization(error.to_string())
}
pub fn is_client_error(&self) -> bool {
matches!(self, Self::Http { status, .. } if * status >= 400 && * status < 500)
}
pub fn is_server_error(&self) -> bool {
matches!(self, Self::Http { status, .. } if * status >= 500 && * status < 600)
}
pub fn is_retryable(&self) -> bool {
match self {
Self::Network(_) => true,
Self::Middleware(_) => true,
Self::Timeout => true,
Self::Http { status, .. } => matches!(status, 429 | 500 | 502 | 503 | 504),
_ => false,
}
}
}
pub type HttpResult<T> = Result<T, HttpError>;
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_retries: u32,
pub initial_delay_ms: u64,
pub max_delay_ms: u64,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries: 3,
initial_delay_ms: 500,
max_delay_ms: 16000,
}
}
}
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct HttpClient {
base_url: String,
api_key: Option<String>,
http_client: ClientWithMiddleware,
custom_headers: BTreeMap<String, String>,
}
impl HttpClient {
pub fn new() -> Self {
Self::with_config(None, true)
}
pub fn with_config(retry_config: Option<RetryConfig>, enable_tracing: bool) -> Self {
let reqwest_client = reqwest::Client::new();
let mut client_builder = ClientBuilder::new(reqwest_client);
if enable_tracing {
use reqwest_tracing::TracingMiddleware;
client_builder = client_builder.with(TracingMiddleware::default());
}
if let Some(config) = retry_config {
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
let retry_policy = ExponentialBackoff::builder()
.retry_bounds(
std::time::Duration::from_millis(config.initial_delay_ms),
std::time::Duration::from_millis(config.max_delay_ms),
)
.build_with_max_retries(config.max_retries);
let retry_middleware = RetryTransientMiddleware::new_with_policy(
retry_policy,
);
client_builder = client_builder.with(retry_middleware);
}
let http_client = client_builder.build();
Self {
base_url: String::new(),
api_key: None,
http_client,
custom_headers: BTreeMap::new(),
}
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_header(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.custom_headers.insert(name.into(), value.into());
self
}
pub fn with_headers(mut self, headers: BTreeMap<String, String>) -> Self {
self.custom_headers.extend(headers);
self
}
}
impl HttpClient {
pub async fn create_todo(&self, request: CreateTodoRequest) -> HttpResult<Todo> {
let url = format!("{}{}", self.base_url, "/todos");
let mut req = self
.http_client
.post(url)
.body(
serde_json::to_vec(&request)
.map_err(|e| HttpError::serialization_error(e))?,
)
.header("content-type", "application/json");
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
let status = response.status();
if status.is_success() {
let body = response
.json()
.await
.map_err(|e| HttpError::deserialization_error(e))?;
Ok(body)
} else {
let status_code = status.as_u16();
let message = status.canonical_reason().unwrap_or("Unknown error");
let body = response.text().await.ok();
Err(HttpError::from_status(status_code, message, body))
}
}
pub async fn delete_todo(&self) -> HttpResult<()> {
let url = format!("{}{}", self.base_url, "/todos/{id}");
let mut req = self.http_client.delete(url);
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
let status = response.status();
if status.is_success() {
let body = response
.json()
.await
.map_err(|e| HttpError::deserialization_error(e))?;
Ok(body)
} else {
let status_code = status.as_u16();
let message = status.canonical_reason().unwrap_or("Unknown error");
let body = response.text().await.ok();
Err(HttpError::from_status(status_code, message, body))
}
}
pub async fn get_todo(&self) -> HttpResult<Todo> {
let url = format!("{}{}", self.base_url, "/todos/{id}");
let mut req = self.http_client.get(url);
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
let status = response.status();
if status.is_success() {
let body = response
.json()
.await
.map_err(|e| HttpError::deserialization_error(e))?;
Ok(body)
} else {
let status_code = status.as_u16();
let message = status.canonical_reason().unwrap_or("Unknown error");
let body = response.text().await.ok();
Err(HttpError::from_status(status_code, message, body))
}
}
pub async fn list_todos(&self) -> HttpResult<()> {
let url = format!("{}{}", self.base_url, "/todos");
let mut req = self.http_client.get(url);
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
let status = response.status();
if status.is_success() {
let body = response
.json()
.await
.map_err(|e| HttpError::deserialization_error(e))?;
Ok(body)
} else {
let status_code = status.as_u16();
let message = status.canonical_reason().unwrap_or("Unknown error");
let body = response.text().await.ok();
Err(HttpError::from_status(status_code, message, body))
}
}
pub async fn update_todo(&self, request: UpdateTodoRequest) -> HttpResult<Todo> {
let url = format!("{}{}", self.base_url, "/todos/{id}");
let mut req = self
.http_client
.put(url)
.body(
serde_json::to_vec(&request)
.map_err(|e| HttpError::serialization_error(e))?,
)
.header("content-type", "application/json");
if let Some(api_key) = &self.api_key {
req = req.bearer_auth(api_key);
}
for (name, value) in &self.custom_headers {
req = req.header(name, value);
}
let response = req.send().await?;
let status = response.status();
if status.is_success() {
let body = response
.json()
.await
.map_err(|e| HttpError::deserialization_error(e))?;
Ok(body)
} else {
let status_code = status.as_u16();
let message = status.canonical_reason().unwrap_or("Unknown error");
let body = response.text().await.ok();
Err(HttpError::from_status(status_code, message, body))
}
}
}