use std::{fmt, future::Future, pin::Pin};
use http::{Method, header::HeaderMap};
use reqwest::redirect::Policy;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HttpMethod {
Get,
}
impl HttpMethod {
fn into_reqwest(self) -> Method {
match self {
Self::Get => Method::GET,
}
}
}
#[derive(Clone)]
pub struct PreparedRequest {
method: HttpMethod,
url: Url,
headers: HeaderMap,
}
impl PreparedRequest {
pub(crate) fn new(method: HttpMethod, url: Url, headers: HeaderMap) -> Self {
Self {
method,
url,
headers,
}
}
pub fn method(&self) -> HttpMethod {
self.method
}
pub fn expose_url(&self) -> &Url {
&self.url
}
pub fn expose_headers(&self) -> &HeaderMap {
&self.headers
}
}
impl fmt::Debug for PreparedRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let header_names: Vec<_> = self.headers.keys().map(|name| name.as_str()).collect();
formatter
.debug_struct("PreparedRequest")
.field("method", &self.method)
.field("url", &"[REDACTED URL]")
.field("header_names", &header_names)
.finish()
}
}
#[derive(Clone)]
pub struct TransportResponse {
status: u16,
headers: HeaderMap,
body: Vec<u8>,
}
impl TransportResponse {
pub fn new(status: u16, headers: HeaderMap, body: impl Into<Vec<u8>>) -> Self {
Self {
status,
headers,
body: body.into(),
}
}
pub fn status(&self) -> u16 {
self.status
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn body(&self) -> &[u8] {
&self.body
}
}
impl fmt::Debug for TransportResponse {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransportResponse")
.field("status", &self.status)
.field("body_bytes", &self.body.len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutorError;
impl ExecutorError {
pub const fn new() -> Self {
Self
}
}
impl Default for ExecutorError {
fn default() -> Self {
Self::new()
}
}
pub type ExecutorFuture<'a> = Pin<
Box<dyn Future<Output = std::result::Result<TransportResponse, ExecutorError>> + Send + 'a>,
>;
pub trait HttpExecutor: fmt::Debug + Send + Sync {
fn execute(&self, request: PreparedRequest) -> ExecutorFuture<'_>;
}
pub(crate) struct ReqwestExecutor {
client: reqwest::Client,
}
impl ReqwestExecutor {
pub(crate) fn new(
timeout: std::time::Duration,
connect_timeout: std::time::Duration,
) -> std::result::Result<Self, ExecutorError> {
let client = reqwest::Client::builder()
.timeout(timeout)
.connect_timeout(connect_timeout)
.redirect(Policy::none())
.build()
.map_err(|_| ExecutorError)?;
Ok(Self { client })
}
}
impl fmt::Debug for ReqwestExecutor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ReqwestExecutor(..)")
}
}
impl HttpExecutor for ReqwestExecutor {
fn execute(&self, request: PreparedRequest) -> ExecutorFuture<'_> {
Box::pin(async move {
let response = self
.client
.request(request.method.into_reqwest(), request.url)
.headers(request.headers)
.send()
.await
.map_err(|_| ExecutorError)?;
let status = response.status().as_u16();
let headers = response.headers().clone();
let body = response.bytes().await.map_err(|_| ExecutorError)?.to_vec();
Ok(TransportResponse::new(status, headers, body))
})
}
}