mod get;
#[cfg(feature = "metrics")]
mod metrics_client;
use std::sync::Arc;
use bytes::Bytes;
pub(crate) use get::get;
use http::{HeaderMap, Request, StatusCode};
#[cfg(feature = "metrics")]
pub use metrics_client::MetricsHttpClient;
use crate::{
error::Error,
platform::{MaybeSendBoxFuture, MaybeSendSync},
};
#[derive(Clone)]
pub struct HttpResponse {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Bytes,
}
impl std::fmt::Debug for HttpResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpResponse")
.field("status", &self.status)
.field("headers", &self.headers)
.field("body_len", &self.body.len())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Idempotency {
Idempotent,
Unknown,
}
pub trait HttpClient: MaybeSendSync {
fn execute(
&self,
request: Request<Bytes>,
idempotency: Idempotency,
) -> MaybeSendBoxFuture<'_, Result<HttpResponse, Error>>;
fn uses_mtls(&self) -> bool {
false
}
}
impl<T: HttpClient + ?Sized> HttpClient for &T {
fn execute(
&self,
request: Request<Bytes>,
idempotency: Idempotency,
) -> MaybeSendBoxFuture<'_, Result<HttpResponse, Error>> {
(**self).execute(request, idempotency)
}
fn uses_mtls(&self) -> bool {
(**self).uses_mtls()
}
}
impl<T: HttpClient + ?Sized> HttpClient for Box<T> {
fn execute(
&self,
request: Request<Bytes>,
idempotency: Idempotency,
) -> MaybeSendBoxFuture<'_, Result<HttpResponse, Error>> {
(**self).execute(request, idempotency)
}
fn uses_mtls(&self) -> bool {
(**self).uses_mtls()
}
}
impl<T: HttpClient + ?Sized> HttpClient for Arc<T> {
fn execute(
&self,
request: Request<Bytes>,
idempotency: Idempotency,
) -> MaybeSendBoxFuture<'_, Result<HttpResponse, Error>> {
(**self).execute(request, idempotency)
}
fn uses_mtls(&self) -> bool {
(**self).uses_mtls()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
struct FakeClient {
status: StatusCode,
body: &'static str,
}
impl HttpClient for FakeClient {
fn execute(
&self,
_request: Request<Bytes>,
_idempotency: Idempotency,
) -> MaybeSendBoxFuture<'_, Result<HttpResponse, Error>> {
Box::pin(async move {
Ok(HttpResponse {
status: self.status,
headers: HeaderMap::new(),
body: Bytes::from_static(self.body.as_bytes()),
})
})
}
}
#[derive(Debug, serde::Deserialize)]
struct Doc {
value: u32,
}
fn uri() -> http::Uri {
http::Uri::from_static("https://example.com/doc")
}
#[tokio::test]
async fn get_deserializes_through_dyn_client() {
let client = FakeClient {
status: StatusCode::OK,
body: r#"{"value": 7}"#,
};
let dyn_client: &dyn HttpClient = &client;
let doc: Doc = get(dyn_client, uri(), HeaderMap::new())
.await
.expect("get succeeds");
assert_eq!(doc.value, 7);
}
#[tokio::test]
async fn get_classifies_bad_status_as_protocol() {
let client = FakeClient {
status: StatusCode::INTERNAL_SERVER_ERROR,
body: "",
};
let err = get::<Doc>(&client, uri(), HeaderMap::new())
.await
.expect_err("non-2xx fails");
assert_eq!(err.kind(), ErrorKind::Protocol);
assert!(!err.is_retryable());
}
#[tokio::test]
async fn erased_clients_still_implement_the_trait() {
fn takes_impl(_: &impl HttpClient) {}
let arc: Arc<dyn HttpClient> = Arc::new(FakeClient {
status: StatusCode::OK,
body: r#"{"value": 1}"#,
});
takes_impl(&arc);
let boxed: Box<dyn HttpClient> = Box::new(FakeClient {
status: StatusCode::OK,
body: r#"{"value": 2}"#,
});
takes_impl(&boxed);
let doc: Doc = get(&arc, uri(), HeaderMap::new()).await.expect("get");
assert_eq!(doc.value, 1);
}
}