use std::future::{Future, ready};
use std::sync::Mutex;
use bytes::Bytes;
use spire_core::backend::{Backend, Client, StatusPolicy};
use spire_core::request::{Request, Response};
use spire_core::{Error, ErrorKind, Result};
use crate::conversion::{head_from_reqwest, request_to_reqwest};
#[derive(Debug, Clone, Default)]
pub struct HttpClient {
client: reqwest::Client,
status_policy: StatusPolicy,
}
impl HttpClient {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_status_policy(mut self, policy: StatusPolicy) -> Self {
self.status_policy = policy;
self
}
#[must_use]
pub const fn as_reqwest(&self) -> &reqwest::Client {
&self.client
}
#[must_use]
pub const fn status_policy(&self) -> StatusPolicy {
self.status_policy
}
}
impl From<reqwest::Client> for HttpClient {
fn from(client: reqwest::Client) -> Self {
Self {
client,
status_policy: StatusPolicy::default(),
}
}
}
impl Backend for HttpClient {
type Client = HttpSession;
fn connect(&self) -> impl Future<Output = Result<Self::Client>> + Send {
ready(Ok(HttpSession {
http: self.clone(),
body: Mutex::new(None),
}))
}
}
#[derive(Debug)]
pub struct HttpSession {
http: HttpClient,
body: Mutex<Option<Bytes>>,
}
impl HttpSession {
#[must_use]
pub const fn http(&self) -> &HttpClient {
&self.http
}
}
impl Client for HttpSession {
async fn fetch(&self, request: &Request) -> Result<Response<()>> {
let uri = request.uri().clone();
let failed = |error: Error| error.with_uri(uri.clone());
let converted = request_to_reqwest(request).map_err(failed)?;
let response = self
.http
.client
.execute(converted)
.await
.map_err(|error| failed(classify(error)))?;
let head = self
.http
.status_policy
.check(head_from_reqwest(&response))?;
let body = response
.bytes()
.await
.map_err(|error| failed(classify(error)))?;
*lock(&self.body) = Some(body);
Ok(head)
}
fn body(&self) -> impl Future<Output = Result<Bytes>> + Send {
let body = lock(&self.body).clone();
ready(body.ok_or_else(|| Error::worker("the request was not fetched yet")))
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn classify(error: reqwest::Error) -> Error {
let kind = if error.is_timeout() {
ErrorKind::Timeout
} else if error.is_builder() {
ErrorKind::Invalid
} else if error.is_redirect() {
ErrorKind::Rejected
} else {
ErrorKind::Http
};
Error::new(kind).with_source(error)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn invalid_request_is_rejected_before_sending() {
let session = HttpClient::new().connect().await.unwrap();
let error = session
.fetch(&Request::new(Bytes::new()))
.await
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::Invalid);
assert!(session.body().await.is_err());
}
#[tokio::test]
async fn sessions_share_the_client() {
let backend = HttpClient::new().with_status_policy(StatusPolicy::Accept);
let session = backend.connect().await.unwrap();
assert_eq!(session.http().status_policy(), StatusPolicy::Accept);
}
}