use core::convert::Infallible;
use core::future::{ready, Future};
use headers::{Header, HeaderMapExt};
use crate::{extract::Extractor, responder::Responder, Request, Response, StatusCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TypedHeader<H>(pub H);
impl_deref!(TypedHeader);
#[skyzen::error]
pub enum TypedHeaderError {
#[error("Missing required header `{0}`", status = StatusCode::BAD_REQUEST)]
Missing(String),
#[error("Header `{0}` is malformed", status = StatusCode::BAD_REQUEST)]
Invalid(String),
}
impl<H: Header + Send + Sync + 'static> Extractor for TypedHeader<H> {
type Error = TypedHeaderError;
fn extract(request: &mut Request) -> impl Future<Output = Result<Self, Self::Error>> + Send {
let name = H::name();
let all = request.headers().get_all(name);
ready(if all.iter().next().is_none() {
Err(TypedHeaderError::Missing(name.to_string()))
} else {
H::decode(&mut all.iter())
.map(Self)
.map_err(|_| TypedHeaderError::Invalid(name.to_string()))
})
}
#[cfg(feature = "openapi")]
fn openapi() -> Option<crate::openapi::ExtractorSchema> {
Some(crate::openapi::ExtractorSchema {
location: crate::openapi::ParameterLocation::Header,
content_type: None,
schema: Some(skyzen_core::openapi::plain_string_schema()),
})
}
}
impl<H: Header + Send + Sync + 'static> Responder for TypedHeader<H> {
type Error = Infallible;
fn respond_to(self, _request: &Request, response: &mut Response) -> Result<(), Self::Error> {
response.headers_mut().typed_insert(self.0);
Ok(())
}
#[cfg(feature = "openapi")]
fn openapi() -> Option<Vec<crate::openapi::ResponseSchema>> {
Some(vec![crate::openapi::ResponseSchema {
status: None,
description: None,
schema: None,
content_type: None,
}])
}
}
#[cfg(test)]
mod tests {
use super::{TypedHeader, TypedHeaderError};
use crate::{header::HeaderValue, Body, Request, Response, StatusCode};
use headers::{ContentType, UserAgent};
use http_kit::HttpError;
use skyzen_core::{Extractor, Responder};
#[tokio::test]
async fn reads_a_typed_request_header() {
let mut request = Request::new(Body::empty());
request.headers_mut().insert(
crate::header::USER_AGENT,
HeaderValue::from_static("skyzen/1"),
);
let TypedHeader(agent) = TypedHeader::<UserAgent>::extract(&mut request)
.await
.unwrap();
assert_eq!(agent.as_str(), "skyzen/1");
}
#[tokio::test]
async fn an_absent_header_names_itself() {
let mut request = Request::new(Body::empty());
let error = TypedHeader::<UserAgent>::extract(&mut request)
.await
.unwrap_err();
assert_eq!(error.status(), StatusCode::BAD_REQUEST);
assert!(matches!(error, TypedHeaderError::Missing(ref name) if name == "user-agent"));
}
#[tokio::test]
async fn a_malformed_header_is_distinguished_from_an_absent_one() {
let mut request = Request::new(Body::empty());
request.headers_mut().insert(
crate::header::CONTENT_TYPE,
HeaderValue::from_static("not a media type"),
);
let error = TypedHeader::<ContentType>::extract(&mut request)
.await
.unwrap_err();
assert!(matches!(error, TypedHeaderError::Invalid(_)), "{error}");
}
#[test]
fn writes_a_typed_response_header() {
let mut response = Response::new(Body::empty());
TypedHeader(ContentType::json())
.respond_to(&Request::new(Body::empty()), &mut response)
.expect("typed headers always render");
assert_eq!(
response.headers().get(crate::header::CONTENT_TYPE).unwrap(),
"application/json"
);
}
}