predawn 0.9.0

Predawn is a Rust web framework like Spring Boot.
Documentation
use std::{collections::BTreeMap, convert::Infallible};

use bytes::Bytes;
use http::{
    header::{HeaderValue, CONTENT_TYPE},
    StatusCode,
};
use mime::{APPLICATION, JSON};
use predawn_core::{
    api_request::ApiRequest,
    api_response::ApiResponse,
    body::RequestBody,
    from_request::FromRequest,
    impl_deref,
    into_response::IntoResponse,
    media_type::{
        has_media_type, MediaType, MultiRequestMediaType, MultiResponseMediaType, RequestMediaType,
        ResponseMediaType, SingleMediaType,
    },
    openapi::{self, Parameter, Schema},
    request::Head,
    response::{MultiResponse, Response, SingleResponse},
};
use predawn_schema::ToSchema;
use serde::{de::DeserializeOwned, Serialize};
use snafu::ResultExt;

use crate::response_error::{
    DeserializeJsonSnafu, InvalidJsonContentTypeSnafu, ReadJsonBytesSnafu, ReadJsonError,
    WriteJsonError, WriteJsonSnafu,
};

#[derive(Debug, Default, Clone, Copy)]
pub struct Json<T>(pub T);

impl_deref!(Json);

impl<'a, T> FromRequest<'a> for Json<T>
where
    T: DeserializeOwned,
{
    type Error = ReadJsonError;

    async fn from_request(head: &'a mut Head, body: RequestBody) -> Result<Self, Self::Error> {
        let content_type = head.content_type().unwrap_or_default();

        if <Self as RequestMediaType>::check_content_type(content_type) {
            let bytes = Bytes::from_request(head, body)
                .await
                .context(ReadJsonBytesSnafu)?;

            let json = crate::util::deserialize_json(&bytes).context(DeserializeJsonSnafu)?;
            Ok(Json(json))
        } else {
            InvalidJsonContentTypeSnafu.fail()
        }
    }
}

impl<T: ToSchema> ApiRequest for Json<T> {
    fn parameters(_: &mut BTreeMap<String, Schema>, _: &mut Vec<String>) -> Option<Vec<Parameter>> {
        None
    }

    fn request_body(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> Option<openapi::RequestBody> {
        Some(openapi::RequestBody {
            content: <Self as MultiRequestMediaType>::content(schemas, schemas_in_progress),
            required: true,
            ..Default::default()
        })
    }
}

impl<T> IntoResponse for Json<T>
where
    T: Serialize,
{
    type Error = WriteJsonError;

    fn into_response(self) -> Result<Response, Self::Error> {
        let mut response = crate::util::serialize_json(&self.0)
            .context(WriteJsonSnafu)?
            .into_response()
            .unwrap_or_else(|a: Infallible| match a {});

        response.headers_mut().insert(
            CONTENT_TYPE,
            HeaderValue::from_static(<Self as MediaType>::MEDIA_TYPE),
        );

        Ok(response)
    }
}

impl<T: ToSchema> ApiResponse for Json<T> {
    fn responses(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> Option<BTreeMap<StatusCode, openapi::Response>> {
        Some(<Self as MultiResponse>::responses(
            schemas,
            schemas_in_progress,
        ))
    }
}

impl<T> MediaType for Json<T> {
    const MEDIA_TYPE: &'static str = "application/json";
}

impl<T> RequestMediaType for Json<T> {
    fn check_content_type(content_type: &str) -> bool {
        has_media_type(
            content_type,
            APPLICATION.as_str(),
            JSON.as_str(),
            JSON.as_str(),
            None,
        )
    }
}

impl<T> ResponseMediaType for Json<T> {}

impl<T: ToSchema> SingleMediaType for Json<T> {
    fn media_type(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> openapi::MediaType {
        openapi::MediaType {
            schema: Some(T::schema_ref(schemas, schemas_in_progress)),
            ..Default::default()
        }
    }
}

impl<T: ToSchema> SingleResponse for Json<T> {
    fn response(
        schemas: &mut BTreeMap<String, Schema>,
        schemas_in_progress: &mut Vec<String>,
    ) -> openapi::Response {
        openapi::Response {
            content: <Self as MultiResponseMediaType>::content(schemas, schemas_in_progress),
            ..Default::default()
        }
    }
}