use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
use http::{Request, Response};
use http_body::Body;
use http_body_util::{BodyExt, Full, combinators::UnsyncBoxBody};
use tower::{Layer, Service};
use jsonapi_core::Error;
use crate::error::error_response_for;
use crate::request::{check_content_type, negotiate};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
type ResponseBody = UnsyncBoxBody<Bytes, BoxError>;
#[derive(Clone, Debug, Default)]
pub struct ContentTypeLayer;
impl ContentTypeLayer {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl<S> Layer<S> for ContentTypeLayer {
type Service = GuardService<S>;
fn layer(&self, inner: S) -> Self::Service {
GuardService {
inner,
guard: Guard {
check_content_type: true,
accept: None,
},
}
}
}
#[derive(Clone, Debug, Default)]
pub struct AcceptLayer {
ext: Vec<String>,
profile: Vec<String>,
}
impl AcceptLayer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn ext(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.ext.extend(uris.into_iter().map(Into::into));
self
}
#[must_use]
pub fn profile(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.profile.extend(uris.into_iter().map(Into::into));
self
}
}
impl<S> Layer<S> for AcceptLayer {
type Service = GuardService<S>;
fn layer(&self, inner: S) -> Self::Service {
GuardService {
inner,
guard: Guard {
check_content_type: false,
accept: Some(AcceptConfig {
ext: self.ext.clone(),
profile: self.profile.clone(),
}),
},
}
}
}
#[derive(Clone, Debug, Default)]
pub struct JsonApiLayer {
ext: Vec<String>,
profile: Vec<String>,
}
impl JsonApiLayer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn ext(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.ext.extend(uris.into_iter().map(Into::into));
self
}
#[must_use]
pub fn profile(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.profile.extend(uris.into_iter().map(Into::into));
self
}
}
impl<S> Layer<S> for JsonApiLayer {
type Service = GuardService<S>;
fn layer(&self, inner: S) -> Self::Service {
GuardService {
inner,
guard: Guard {
check_content_type: true,
accept: Some(AcceptConfig {
ext: self.ext.clone(),
profile: self.profile.clone(),
}),
},
}
}
}
#[derive(Clone, Debug)]
struct AcceptConfig {
ext: Vec<String>,
profile: Vec<String>,
}
#[derive(Clone, Debug)]
struct Guard {
check_content_type: bool,
accept: Option<AcceptConfig>,
}
#[derive(Clone, Debug)]
pub struct GuardService<S> {
inner: S,
guard: Guard,
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for GuardService<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ReqBody: Send + 'static,
ResBody: Body<Data = Bytes> + Send + 'static,
ResBody::Error: Into<BoxError>,
{
type Response = Response<ResponseBody>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, S::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
let guard = self.guard.clone();
Box::pin(async move {
if guard.check_content_type
&& request_carries_body(req.method())
&& let Err(err) = check_content_type(req.headers())
{
return Ok(reject(&err));
}
if let Some(accept) = guard.accept {
let ext: Vec<&str> = accept.ext.iter().map(String::as_str).collect();
let profile: Vec<&str> = accept.profile.iter().map(String::as_str).collect();
match negotiate(req.headers(), &ext, &profile) {
Ok(media) => {
req.extensions_mut().insert(media);
}
Err(err) => return Ok(reject(&err)),
}
}
let response = inner.call(req).await?;
Ok(response.map(box_inner))
})
}
}
fn request_carries_body(method: &http::Method) -> bool {
matches!(
*method,
http::Method::POST | http::Method::PUT | http::Method::PATCH
)
}
fn reject(err: &Error) -> Response<ResponseBody> {
error_response_for(err).map(box_bytes)
}
fn box_bytes(bytes: Bytes) -> ResponseBody {
Full::new(bytes)
.map_err(|never| match never {})
.boxed_unsync()
}
fn box_inner<B>(body: B) -> ResponseBody
where
B: Body<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
body.map_err(Into::into).boxed_unsync()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::JSON_API_MEDIA_TYPE;
use http::{StatusCode, header};
use jsonapi_core::JsonApiMediaType;
use std::convert::Infallible;
use tower::ServiceExt;
macro_rules! ok_service {
() => {
tower::service_fn(|_req: Request<Full<Bytes>>| async {
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(b"ok"))))
})
};
}
fn request_with(method: &str, headers: &[(header::HeaderName, &str)]) -> Request<Full<Bytes>> {
let mut builder = Request::builder().method(method).uri("/articles");
for (name, value) in headers {
builder = builder.header(name.clone(), *value);
}
builder.body(Full::new(Bytes::new())).unwrap()
}
async fn body_string(response: Response<ResponseBody>) -> String {
let bytes = response.into_body().collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[test]
fn content_type_layer_passes_conforming_post() {
pollster::block_on(async {
let svc = ContentTypeLayer::new().layer(ok_service!());
let req = request_with("POST", &[(header::CONTENT_TYPE, JSON_API_MEDIA_TYPE)]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(body_string(res).await, "ok");
});
}
#[test]
fn content_type_layer_rejects_wrong_type_post_with_415() {
pollster::block_on(async {
let svc = ContentTypeLayer::new().layer(ok_service!());
let req = request_with("POST", &[(header::CONTENT_TYPE, "application/json")]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let body = body_string(res).await;
assert!(body.contains("\"errors\""), "body: {body}");
});
}
#[test]
fn content_type_layer_skips_bodyless_get() {
pollster::block_on(async {
let svc = ContentTypeLayer::new().layer(ok_service!());
let req = request_with("GET", &[]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
});
}
#[test]
fn accept_layer_rejects_unacceptable_with_406() {
pollster::block_on(async {
let svc = AcceptLayer::new().layer(ok_service!());
let req = request_with("GET", &[(header::ACCEPT, "text/html")]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
});
}
#[test]
fn accept_layer_inserts_negotiated_media_type_into_extensions() {
pollster::block_on(async {
let inner = tower::service_fn(|req: Request<Full<Bytes>>| async move {
let body = if req.extensions().get::<JsonApiMediaType>().is_some() {
"present"
} else {
"absent"
};
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from_static(
body.as_bytes(),
))))
});
let svc = AcceptLayer::new().layer(inner);
let req = request_with("GET", &[(header::ACCEPT, JSON_API_MEDIA_TYPE)]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(body_string(res).await, "present");
});
}
#[test]
fn accept_layer_negotiates_advertised_ext_into_extensions() {
pollster::block_on(async {
const EXT: &str = "https://jsonapi.org/ext/atomic";
let inner = tower::service_fn(|req: Request<Full<Bytes>>| async move {
let ext = req
.extensions()
.get::<JsonApiMediaType>()
.map(|m| m.ext.join(","))
.unwrap_or_default();
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(ext))))
});
let svc = AcceptLayer::new().ext([EXT]).layer(inner);
let req = request_with(
"GET",
&[(
header::ACCEPT,
&format!("{JSON_API_MEDIA_TYPE}; ext=\"{EXT}\""),
)],
);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(body_string(res).await, EXT);
});
}
#[test]
fn accept_layer_drops_unadvertised_ext() {
pollster::block_on(async {
let inner = tower::service_fn(|req: Request<Full<Bytes>>| async move {
let ext = req
.extensions()
.get::<JsonApiMediaType>()
.map(|m| m.ext.join(","))
.unwrap_or_else(|| "MISSING".to_string());
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(ext))))
});
let svc = AcceptLayer::new().layer(inner);
let req = request_with(
"GET",
&[(
header::ACCEPT,
&format!("{JSON_API_MEDIA_TYPE}; ext=\"https://unadvertised\""),
)],
);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(body_string(res).await, "", "requested ext must be dropped");
});
}
#[test]
fn content_type_layer_enforces_on_put_and_patch() {
pollster::block_on(async {
for method in ["PUT", "PATCH"] {
let svc = ContentTypeLayer::new().layer(ok_service!());
let req = request_with(method, &[(header::CONTENT_TYPE, "application/json")]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(
res.status(),
StatusCode::UNSUPPORTED_MEDIA_TYPE,
"{method} with wrong content-type must 415"
);
}
});
}
#[test]
fn accept_layer_passes_json_api() {
pollster::block_on(async {
let svc = AcceptLayer::new().layer(ok_service!());
let req = request_with("GET", &[(header::ACCEPT, JSON_API_MEDIA_TYPE)]);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
});
}
#[test]
fn json_api_layer_checks_content_type_before_accept() {
pollster::block_on(async {
let svc = JsonApiLayer::new().layer(ok_service!());
let req = request_with(
"POST",
&[
(header::CONTENT_TYPE, "application/json"),
(header::ACCEPT, "text/html"),
],
);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
});
}
#[test]
fn json_api_layer_passes_fully_conforming_post() {
pollster::block_on(async {
let svc = JsonApiLayer::new().layer(ok_service!());
let req = request_with(
"POST",
&[
(header::CONTENT_TYPE, JSON_API_MEDIA_TYPE),
(header::ACCEPT, JSON_API_MEDIA_TYPE),
],
);
let res = svc.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(body_string(res).await, "ok");
});
}
}