use super::AsyncConfig;
use crate::commands;
use crate::core::{Cmd, Message};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
}
#[derive(Debug, Clone)]
pub enum HttpError {
NetworkError(String),
Timeout,
InvalidUrl(String),
ServerError(u16, String),
ParseError(String),
}
impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NetworkError(e) => write!(f, "Network error: {e}"),
Self::Timeout => write!(f, "Request timed out"),
Self::InvalidUrl(url) => write!(f, "Invalid URL: {url}"),
Self::ServerError(code, msg) => write!(f, "Server error {code}: {msg}"),
Self::ParseError(e) => write!(f, "Parse error: {e}"),
}
}
}
impl std::error::Error for HttpError {}
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: String,
pub bytes: Vec<u8>,
}
pub fn http_get<M, F>(url: impl Into<String>, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<HttpResponse, HttpError>) -> M + Send + 'static,
{
http_request(HttpMethod::Get, url, None::<String>, None, handler)
}
pub fn http_post<M, F, B>(url: impl Into<String>, body: B, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<HttpResponse, HttpError>) -> M + Send + 'static,
B: Into<String>,
{
let mut headers = HashMap::new();
headers.insert("Content-Type".to_string(), "application/json".to_string());
http_request(HttpMethod::Post, url, Some(body), Some(headers), handler)
}
pub fn http_request<M, F, B>(
method: HttpMethod,
url: impl Into<String>,
body: Option<B>,
headers: Option<HashMap<String, String>>,
handler: F,
) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<HttpResponse, HttpError>) -> M + Send + 'static,
B: Into<String>,
{
let url = url.into();
let body = body.map(std::convert::Into::into);
commands::spawn(async move {
let result = perform_http_request(method, url, body, headers).await;
Some(handler(result))
})
}
async fn perform_http_request(
method: HttpMethod,
url: String,
body: Option<String>,
headers: Option<HashMap<String, String>>,
) -> Result<HttpResponse, HttpError> {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(HttpResponse {
status: 200,
headers: headers.unwrap_or_default(),
body: body.unwrap_or_else(|| {
format!(
"Mock response for {} {}",
match method {
HttpMethod::Get => "GET",
HttpMethod::Post => "POST",
HttpMethod::Put => "PUT",
HttpMethod::Delete => "DELETE",
HttpMethod::Patch => "PATCH",
HttpMethod::Head => "HEAD",
},
url
)
}),
bytes: Vec::new(),
})
}
pub fn http_with_retry<M, F>(
method: HttpMethod,
url: impl Into<String>,
config: AsyncConfig,
handler: F,
) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<HttpResponse, HttpError>) -> M + Send + 'static,
{
let url = url.into();
commands::spawn(async move {
let mut attempts = 0;
loop {
let result = perform_http_request(method, url.clone(), None, None).await;
if result.is_ok() || attempts >= config.retries {
return Some(handler(result));
}
attempts += 1;
match config.backoff {
super::BackoffStrategy::None => {}
super::BackoffStrategy::Linear(duration) => {
tokio::time::sleep(duration * attempts).await;
}
super::BackoffStrategy::Exponential(duration) => {
tokio::time::sleep(duration * 2u32.pow(attempts)).await;
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_http_error_display_basic(error_msg in "[a-zA-Z0-9 ]{1,50}") {
let network_error = HttpError::NetworkError(error_msg.clone());
let network_display = network_error.to_string();
prop_assert!(!network_display.is_empty());
prop_assert!(network_display.contains(&error_msg));
let parse_error = HttpError::ParseError(error_msg.clone());
let parse_display = parse_error.to_string();
prop_assert!(!parse_display.is_empty());
prop_assert!(parse_display.contains(&error_msg));
let timeout_error = HttpError::Timeout;
let timeout_display = timeout_error.to_string();
prop_assert!(!timeout_display.is_empty());
prop_assert!(timeout_display.to_lowercase().contains("timed out"));
}
}
proptest! {
#[test]
fn prop_server_error_display(status_code in 400u16..600u16, message in "[a-zA-Z0-9 ]{1,30}") {
let error = HttpError::ServerError(status_code, message.clone());
let display = error.to_string();
prop_assert!(!display.is_empty());
prop_assert!(display.contains(&status_code.to_string()));
prop_assert!(display.contains(&message));
}
}
}