use std::future::Future;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::header::HeaderMap;
use hyper::{Method, Response, StatusCode, Uri};
use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::rt::TokioExecutor;
use crate::error::{Error, Result};
pub trait HttpTransport: Clone + Send + Sync + 'static {
fn send(
&self,
method: Method,
uri: Uri,
body: Option<Bytes>,
headers: &HeaderMap,
) -> impl Future<Output = Result<(StatusCode, Bytes)>> + Send;
}
type Connector = hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>;
#[derive(Clone)]
pub struct HttpClient {
inner: HyperClient<Connector, Full<Bytes>>,
}
impl HttpClient {
pub fn new() -> Self {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_only()
.enable_http1()
.build();
let inner = HyperClient::builder(TokioExecutor::new()).build(https);
Self { inner }
}
}
impl HttpTransport for HttpClient {
async fn send(
&self,
method: Method,
uri: Uri,
body: Option<Bytes>,
headers: &HeaderMap,
) -> Result<(StatusCode, Bytes)> {
let mut builder = hyper::Request::builder().method(method).uri(uri);
for (key, value) in headers {
builder = builder.header(key, value);
}
let req = builder
.body(Full::new(body.unwrap_or_default()))
.map_err(|e: hyper::http::Error| Error::Other(e.to_string()))?;
let resp: Response<Incoming> = self.inner.request(req).await?;
let status = resp.status();
let resp_body = resp.into_body().collect().await?.to_bytes();
Ok((status, resp_body))
}
}
#[cfg(test)]
pub(crate) mod mock {
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use hyper::header::HeaderMap;
use hyper::{Method, StatusCode, Uri};
use crate::error::Result;
use super::HttpTransport;
#[derive(Debug, Clone)]
pub struct RecordedRequest {
pub method: Method,
pub uri: Uri,
pub headers: HeaderMap,
pub body: Bytes,
}
#[derive(Clone)]
pub struct MockResponse {
pub status: StatusCode,
pub body: Bytes,
}
impl MockResponse {
pub fn ok(body: impl Into<Bytes>) -> Self {
Self {
status: StatusCode::OK,
body: body.into(),
}
}
pub fn error(status: StatusCode, body: impl Into<Bytes>) -> Self {
Self {
status,
body: body.into(),
}
}
}
#[derive(Clone)]
pub struct MockHttp {
responses: Arc<Mutex<Vec<MockResponse>>>,
pub requests: Arc<Mutex<Vec<RecordedRequest>>>,
}
impl MockHttp {
pub fn new(responses: Vec<MockResponse>) -> Self {
Self {
responses: Arc::new(Mutex::new(responses)),
requests: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn recorded_requests(&self) -> Vec<RecordedRequest> {
self.requests.lock().unwrap().clone()
}
fn next_response(&self) -> MockResponse {
let mut queue = self.responses.lock().unwrap();
assert!(
!queue.is_empty(),
"MockHttp: unexpected call — response queue is empty"
);
queue.remove(0)
}
}
impl HttpTransport for MockHttp {
async fn send(
&self,
method: Method,
uri: Uri,
body: Option<Bytes>,
headers: &HeaderMap,
) -> Result<(StatusCode, Bytes)> {
self.requests.lock().unwrap().push(RecordedRequest {
method,
uri,
headers: headers.clone(),
body: body.unwrap_or_default(),
});
let r = self.next_response();
Ok((r.status, r.body))
}
}
}