tork-core 0.1.0

Core runtime for the Tork web framework: HTTP server, routing, dependency injection, responses, and errors, built on Hyper and Tokio.
Documentation
//! Response types and the [`IntoResponse`] conversion trait.

use bytes::Bytes;
use http::header::{HeaderValue, CONTENT_TYPE};
use http::StatusCode;

use crate::body::RespBody;

pub mod json;

pub use json::{json_response, Json};

/// The concrete HTTP response type used throughout the framework.
pub type Response = http::Response<RespBody>;

/// Converts a value into an HTTP [`Response`].
///
/// Implemented by the response building blocks ([`Json`], [`Error`], status
/// codes) and by [`Result`], so handlers can return high-level values and have
/// them rendered into a proper HTTP response by the generated route glue.
///
/// [`Error`]: crate::Error
/// [`Result`]: core::result::Result
pub trait IntoResponse {
    /// Consumes `self` and produces the HTTP response to send to the client.
    fn into_response(self) -> Response;
}

/// Builds a response with the given status, `Content-Type`, and body.
pub(crate) fn with_body(status: StatusCode, content_type: &'static str, body: Bytes) -> Response {
    let mut response = http::Response::new(RespBody::new(body));
    *response.status_mut() = status;
    response
        .headers_mut()
        .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
    response
}

/// Builds a response from a pre-serialized body and an explicit content type.
///
/// Useful for serving content that is generated once and cached, such as the
/// OpenAPI document or a static documentation asset.
pub fn bytes_response(status: StatusCode, content_type: &'static str, body: Bytes) -> Response {
    with_body(status, content_type, body)
}

/// Splits a response into its head and fully-buffered body bytes.
///
/// Used by middleware that needs to inspect or rewrite a response body (such as
/// compression). The body is `Full<Bytes>`, so collecting it is immediate.
pub(crate) async fn into_body_bytes(response: Response) -> (http::response::Parts, Bytes) {
    use http_body_util::BodyExt;
    let (parts, body) = response.into_parts();
    let bytes = body
        .collect()
        .await
        .map(|collected| collected.to_bytes())
        .unwrap_or_default();
    (parts, bytes)
}

/// Builds an empty-bodied response carrying only a status code.
pub(crate) fn empty(status: StatusCode) -> Response {
    let mut response = http::Response::new(RespBody::new(Bytes::new()));
    *response.status_mut() = status;
    response
}

impl IntoResponse for Response {
    fn into_response(self) -> Response {
        self
    }
}

impl IntoResponse for StatusCode {
    fn into_response(self) -> Response {
        empty(self)
    }
}

impl IntoResponse for () {
    fn into_response(self) -> Response {
        empty(StatusCode::OK)
    }
}

impl<T, E> IntoResponse for core::result::Result<T, E>
where
    T: IntoResponse,
    E: Into<crate::error::Error>,
{
    fn into_response(self) -> Response {
        match self {
            Ok(value) => value.into_response(),
            Err(error) => error.into().into_response(),
        }
    }
}

/// Renders a handler's result into a response, serializing the success value to
/// JSON with the given status code.
///
/// This is generated-code support, not part of the user-facing API. Handlers
/// return `Result<T>` where `T` is serializable; the route macro feeds that
/// result and the declared success status here.
///
/// A handler error is returned as `Err` rather than rendered here, so that the
/// dispatch boundary can run lifecycle hooks and exception handlers against the
/// error value before it becomes a response.
#[doc(hidden)]
pub fn __finish<T, E>(
    result: core::result::Result<T, E>,
    status: StatusCode,
) -> crate::error::Result<Response>
where
    T: serde::Serialize,
    E: Into<crate::error::Error>,
{
    match result {
        Ok(value) => Ok(json::json_response(status, &value)),
        Err(error) => Err(error.into()),
    }
}

/// Like [`__finish`], but converts the success value into the declared response
/// model `U` (via `From`) before serializing.
///
/// Generated by the route macro when `response_model` differs from the handler's
/// return type. With `response_model` equal to the return type the conversion is
/// the free identity `From<T> for T`.
#[doc(hidden)]
pub fn __finish_into<T, U, E>(
    result: core::result::Result<T, E>,
    status: StatusCode,
) -> crate::error::Result<Response>
where
    U: From<T> + serde::Serialize,
    E: Into<crate::error::Error>,
{
    match result {
        Ok(value) => Ok(json::json_response(status, &U::from(value))),
        Err(error) => Err(error.into()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::Error;
    use bytes::Bytes;
    use serde::Serialize;

    #[derive(Serialize)]
    struct Payload {
        id: i64,
    }

    #[derive(Serialize)]
    struct RawPayload {
        id: i64,
    }

    impl From<RawPayload> for Payload {
        fn from(value: RawPayload) -> Self {
            Self { id: value.id }
        }
    }

    #[tokio::test]
    async fn status_code_and_unit_into_response_are_empty() {
        let status = StatusCode::NO_CONTENT.into_response();
        let unit = ().into_response();

        let (status_parts, status_body) = into_body_bytes(status).await;
        let (unit_parts, unit_body) = into_body_bytes(unit).await;

        assert_eq!(status_parts.status, StatusCode::NO_CONTENT);
        assert_eq!(status_body, Bytes::new());
        assert_eq!(unit_parts.status, StatusCode::OK);
        assert_eq!(unit_body, Bytes::new());
    }

    #[tokio::test]
    async fn finish_helpers_serialize_success_values() {
        let direct = __finish::<_, Error>(Ok(Payload { id: 7 }), StatusCode::CREATED).unwrap();
        let mapped =
            __finish_into::<_, Payload, Error>(Ok(RawPayload { id: 9 }), StatusCode::ACCEPTED)
                .unwrap();

        let (direct_parts, direct_body) = into_body_bytes(direct).await;
        let (mapped_parts, mapped_body) = into_body_bytes(mapped).await;

        assert_eq!(direct_parts.status, StatusCode::CREATED);
        assert_eq!(mapped_parts.status, StatusCode::ACCEPTED);
        assert_eq!(direct_body, Bytes::from_static(br#"{"id":7}"#));
        assert_eq!(mapped_body, Bytes::from_static(br#"{"id":9}"#));
    }

    #[test]
    fn result_into_response_and_finish_helpers_propagate_errors() {
        let error = Error::bad_request("bad");

        let response =
            core::result::Result::<StatusCode, _>::Err(Error::bad_request("bad")).into_response();
        assert_eq!(response.status(), StatusCode::BAD_REQUEST);

        let finish = __finish::<Payload, _>(Err(error), StatusCode::OK)
            .err()
            .expect("expected handler error");
        assert_eq!(finish.message(), "bad");
    }
}