use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
use http::{HeaderMap, Method, StatusCode};
use url::Url;
pub type Authorized<'a> =
Pin<Box<dyn Future<Output = Result<HeaderMap, AuthorizerError>> + Send + 'a>>;
pub trait Authorizer: Send + Sync + fmt::Debug {
fn authorize<'a>(&'a self, method: &'a Method, url: &'a Url) -> Authorized<'a>;
fn answered(&self, url: &Url, status: StatusCode, headers: &HeaderMap) -> Retry;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Retry {
Done,
Resend,
}
#[derive(Debug, thiserror::Error)]
#[error("the authorizer could not make the headers of the request")]
pub struct AuthorizerError {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
}
impl AuthorizerError {
#[must_use]
pub fn new(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Self {
source: source.into(),
}
}
}
#[derive(Debug)]
pub(crate) enum Unsent {
Unauthenticated(AuthorizerError),
Timeout,
Transport(reqwest::Error),
}
pub(crate) async fn send(
http: &reqwest::Client,
authorizer: Option<&dyn Authorizer>,
mut request: reqwest::Request,
timeout: Duration,
) -> Result<reqwest::Response, Unsent> {
let Some(authorizer) = authorizer else {
*request.timeout_mut() = Some(timeout);
return http.execute(request).await.map_err(Unsent::Transport);
};
let deadline = Instant::now().checked_add(timeout);
let mut target = request.url().clone();
target.set_query(None);
target.set_fragment(None);
let mut resent = false;
loop {
let spare = if resent { None } else { request.try_clone() };
let headers = authorizer
.authorize(request.method(), &target)
.await
.map_err(Unsent::Unauthenticated)?;
request.headers_mut().extend(headers);
let remaining = deadline
.and_then(|deadline| deadline.checked_duration_since(Instant::now()))
.filter(|remaining| !remaining.is_zero())
.ok_or(Unsent::Timeout)?;
*request.timeout_mut() = Some(remaining);
let response = http.execute(request).await.map_err(Unsent::Transport)?;
let retry = authorizer.answered(&target, response.status(), response.headers());
match (retry, spare) {
(Retry::Resend, Some(spare)) => {
request = spare;
resent = true;
}
_ => return Ok(response),
}
}
}