use crate::{ContentTypeError, Error, StatusError};
use http_body_util::BodyExt;
use std::pin::Pin;
use std::task::{Context, Poll, ready};
#[pin_project::pin_project(project = SendProj)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Send<Fut, B, E>
where
Fut: Future<Output = Result<http::Response<B>, E>>,
B: http_body::Body,
{
S0 {
#[pin]
f: Fut,
expected: Option<(http::StatusCode, Option<mime::Mime>)>,
},
S1 {
e: Option<Error<E, B::Error>>,
},
S2 {
#[pin]
f: http_body_util::combinators::Collect<B>,
parts: Option<http::response::Parts>,
kind: ErrorKind,
},
}
pub(crate) enum ErrorKind {
ContentType,
Status,
}
impl<Fut, B, E> Send<Fut, B, E>
where
Fut: Future<Output = Result<http::Response<B>, E>>,
B: http_body::Body,
{
pub(crate) fn new<C, R>(
client: C,
request: R,
expected: (http::StatusCode, Option<mime::Mime>),
) -> Self
where
C: FnOnce(http::Request<String>) -> Fut,
R: FnOnce() -> Result<http::Request<String>, Error<E, B::Error>>,
{
match request() {
Ok(request) => {
tracing::debug!(?request);
Self::S0 {
f: client(request),
expected: Some(expected),
}
}
Err(e) => Self::S1 { e: Some(e) },
}
}
}
impl<Fut, B, E> Future for Send<Fut, B, E>
where
Fut: Future<Output = Result<http::Response<B>, E>>,
B: http_body::Body,
{
type Output = Result<http::Response<B>, Error<E, B::Error>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
match self.as_mut().project() {
SendProj::S0 { f, expected } => {
let response = ready!(f.poll(cx)).map_err(Error::Client)?;
let expected = expected.take().unwrap();
let content_type = content_type(&response);
let kind = if response.status() != expected.0 {
ErrorKind::Status
} else if expected.1.is_some_and(|expected| {
content_type.as_ref().map(mime::Mime::essence_str)
!= Some(expected.essence_str())
}) {
ErrorKind::ContentType
} else {
break Poll::Ready(Ok(response));
};
let (parts, body) = response.into_parts();
self.set(Self::S2 {
f: body.collect(),
parts: Some(parts),
kind,
});
}
SendProj::S1 { e } => {
break Poll::Ready(Err(e.take().unwrap()));
}
SendProj::S2 { f, parts, kind } => {
let body = ready!(f.poll(cx)).map_err(Error::Body)?;
let response =
http::Response::from_parts(parts.take().unwrap(), body.to_bytes());
tracing::debug!(?response);
break Poll::Ready(Err(match kind {
ErrorKind::ContentType => Error::ContentType(ContentTypeError(response)),
ErrorKind::Status => Error::Status(StatusError(response)),
}));
}
}
}
}
}
pub(crate) fn content_type<B>(response: &http::Response<B>) -> Option<mime::Mime> {
response
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()?.parse().ok())
}