use std::future::{Future, ready};
use std::sync::Arc;
use spire_core::backend::{Backend, Session, StatusPolicy};
use spire_core::http::{Request, Response};
use spire_core::identity::Identity;
use spire_core::{Error, ErrorKind, Result};
use crate::clients::Clients;
use crate::conversion::{head_from_reqwest, request_to_reqwest};
#[derive(Debug, Clone, Default)]
pub struct ReqwestBackend {
clients: Clients,
status_policy: StatusPolicy,
}
impl ReqwestBackend {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_builder<F>(builder: F) -> Result<Self>
where
F: Fn() -> reqwest::ClientBuilder + Send + Sync + 'static,
{
Ok(Self {
clients: Clients::new(Arc::new(builder))?,
status_policy: StatusPolicy::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.clients.direct()
}
#[must_use]
pub const fn status_policy(&self) -> StatusPolicy {
self.status_policy
}
}
impl Backend for ReqwestBackend {
type Session = ReqwestSession;
fn connect(&self) -> impl Future<Output = Result<Self::Session>> + Send {
ready(Ok(self.session(self.clients.direct().clone())))
}
fn connect_as(
&self,
identity: &Identity,
) -> impl Future<Output = Result<Self::Session>> + Send {
ready(self.clients.of(identity).map(|client| self.session(client)))
}
}
impl ReqwestBackend {
fn session(&self, client: reqwest::Client) -> ReqwestSession {
ReqwestSession {
http: self.clone(),
client,
}
}
}
#[derive(Debug)]
pub struct ReqwestSession {
http: ReqwestBackend,
client: reqwest::Client,
}
impl ReqwestSession {
#[must_use]
pub const fn http(&self) -> &ReqwestBackend {
&self.http
}
}
impl Session for ReqwestSession {
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
.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)))?;
Ok(head.map(|()| body))
}
}
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 = ReqwestBackend::new().connect().await.unwrap();
let error = session.fetch(&Request::default()).await.unwrap_err();
assert_eq!(error.kind(), ErrorKind::Invalid);
}
#[tokio::test]
async fn sessions_share_the_client() {
let backend = ReqwestBackend::new().with_status_policy(StatusPolicy::Accept);
let session = backend.connect().await.unwrap();
assert_eq!(session.http().status_policy(), StatusPolicy::Accept);
}
}