quokka-handler 0.3.0-beta.0

Handler helpers for Quokka
Documentation
use std::{future::Future, marker::PhantomData, pin::Pin};

use axum::{
    extract::{FromRequest, FromRequestParts, Request},
    handler::Handler,
    response::{IntoResponse, Response},
    routing::{get, MethodRouter},
};
use quokka_state::ProvideState;
use quokka_templating::Templating;

///
/// Handles a POST request with the [axum::extract::Form] extractor and passes it to the provided [DataHandler] (in H).
///
/// The [Self::response_renderer] will be used for rendering a response, it will be provided with a [HandlerError]<[DataHandler::Error]> as
/// an [axum::extract::Extension] so it can react to errors during handling. Additionally it will be provided with the
/// [DataHandler::Extension] so that additional data can be used for communicating to the user.
///
/// **Note:** When using the `Into::into` call to convert this handler into a [MethodRouter] for axum, the [Self::response_renderer] will also
/// be used for the [MethodRouter::get] call
///
#[derive(Clone)]
pub struct FormHandler<H, R> {
    response_renderer: R,
    _processor: PhantomData<H>,
}

///
/// Handles a POST request with the [axum::extract::Json] extractor and passes it to the provided [DataHandler] (in H).
///
/// The [Self::response_renderer] will be used for rendering a response, it will be provided with a [HandlerError]<[DataHandler::Error]> as
/// an [axum::extract::Extension] so it can react to errors during handling. Additionally it will be provided with the
/// [DataHandler::Extension] so that additional data can be used for communicating to the user.
///
/// **Note:** When using the `Into::into` call to convert this handler into a [MethodRouter] for axum, the [Self::response_renderer] will also
/// be used for the [MethodRouter::get] call
///
#[derive(Clone)]
pub struct JsonHandler<P, R> {
    response_renderer: R,
    _processor: PhantomData<P>,
}

#[derive(Clone, Debug, thiserror::Error)]
pub enum HandlerError<E> {
    #[error("{0}")]
    DataHandlerError(E),
    #[error("Unable to extract data")]
    DataExtractorRejection(crate::Error),
    #[error("Unable to extract parts")]
    ParamExtractorRejection(crate::Error),
}

///
/// Handles incoming data
///
/// - [Self::Args] - This can be used to receive Args from the request using anything that is [axum::extract::FromRequestParts]
/// - [Self::Body] - The data which should be received through the request. This is supposed to be the struct, not the axum extractor.
/// - [Self::Error] - The error type which gets emitted when something fails
/// - [Self::Extension] - Anything that can contain additional data to indicate success (like a message)
///
pub trait DataHandler<S> {
    type Args: FromRequestParts<S> + Send + Sync;
    type Body: serde::de::DeserializeOwned + 'static;
    type Error: std::error::Error + Send;
    type Extension: Clone + Send + Sync + 'static;

    fn process_data(
        &self,
        params: Self::Args,
        body: Self::Body,
    ) -> impl Future<Output = Result<Self::Extension, Self::Error>> + Send;
}

impl<P, R> FormHandler<P, R> {
    pub fn new(response_renderer: R) -> Self {
        Self {
            response_renderer,
            _processor: PhantomData,
        }
    }
}

impl<P, R> JsonHandler<P, R> {
    pub fn new(response_renderer: R) -> Self {
        Self {
            response_renderer,
            _processor: PhantomData,
        }
    }
}

impl<S, H, R, T> Handler<T, S> for FormHandler<H, R>
where
    Self: Send,
    T: 'static,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<H>,
    H: DataHandler<S> + Clone + Send + Sync + 'static,
    <<H as DataHandler<S>>::Args as FromRequestParts<S>>::Rejection: Send + Sync,
    <H as DataHandler<S>>::Body: Send,
    <H as DataHandler<S>>::Error: Clone + Send + Sync + 'static,
    R: Clone + Send + Sync + 'static,
    R: Handler<T, S>,
{
    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

    #[tracing::instrument(skip_all)]
    fn call(self, request: Request, state: S) -> Self::Future {
        let processor: H = state.provide();
        let renderer = self.response_renderer.clone();

        Box::pin(async move {
            let (mut parts, body) = request.into_parts();
            let request = Request::from_parts(parts.clone(), body);

            match process_data_call::<_, _, axum::extract::Form<H::Body>>(
                &processor, request, &state,
            )
            .await
            {
                Err(error) => {
                    tracing::error!(
                        ?error,
                        handler = std::any::type_name::<H>(),
                        "Unable to process data with handler"
                    );
                    parts.extensions.insert(error);
                }
                Ok(extension) => {
                    parts.extensions.insert(extension);
                }
            }

            R::call(renderer, Request::from_parts(parts, ().into()), state).await
        })
    }
}

impl<S, H, R> From<FormHandler<H, R>> for MethodRouter<S>
where
    Self: Send,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<H>,
    H: DataHandler<S> + Clone + Send + Sync + 'static,
    <<H as DataHandler<S>>::Args as FromRequestParts<S>>::Rejection: Send + Sync,
    <H as DataHandler<S>>::Body: Send,
    <H as DataHandler<S>>::Error: Clone + Send + Sync + 'static,
    R: Clone + Send + Sync + 'static,
    R: Handler<Response, S>,
{
    fn from(val: FormHandler<H, R>) -> Self {
        get::<_, Response, _>(val.response_renderer.clone()).post(val)
    }
}

impl<S, H, R, T> Handler<T, S> for JsonHandler<H, R>
where
    Self: Send,
    T: 'static,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<H>,
    H: DataHandler<S> + Clone + Send + Sync + 'static,
    <<H as DataHandler<S>>::Args as FromRequestParts<S>>::Rejection: Send + Sync,
    <H as DataHandler<S>>::Body: Send,
    <H as DataHandler<S>>::Error: Clone + Send + Sync + 'static,
    R: Clone + Send + Sync + 'static,
    R: Handler<T, S>,
{
    type Future = Pin<Box<dyn Future<Output = Response> + Send>>;

    #[tracing::instrument(skip_all)]
    fn call(self, request: Request, state: S) -> Self::Future {
        let processor: H = state.provide();
        let renderer = self.response_renderer.clone();

        Box::pin(async move {
            let (mut parts, body) = request.into_parts();
            let request = Request::from_parts(parts.clone(), body);

            match process_data_call::<_, _, axum::extract::Json<H::Body>>(
                &processor, request, &state,
            )
            .await
            {
                Err(error) => {
                    tracing::error!(
                        ?error,
                        handler = std::any::type_name::<H>(),
                        "Unable to process data with handler"
                    );
                    parts.extensions.insert(error);
                }
                Ok(extension) => {
                    parts.extensions.insert(extension);
                }
            }

            R::call(renderer, Request::from_parts(parts, ().into()), state).await
        })
    }
}

impl<S, H, R> From<JsonHandler<H, R>> for MethodRouter<S>
where
    Self: Send,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<H>,
    H: DataHandler<S> + Clone + Send + Sync + 'static,
    <<H as DataHandler<S>>::Args as FromRequestParts<S>>::Rejection: Send + Sync,
    <H as DataHandler<S>>::Body: Send,
    <H as DataHandler<S>>::Error: Clone + Send + Sync + 'static,
    R: Clone + Send + Sync + 'static,
    R: Handler<Response, S>,
{
    fn from(val: JsonHandler<H, R>) -> Self {
        get::<_, Response, _>(val.response_renderer.clone()).post(val)
    }
}

trait DataExtractor<B, S> {
    type Rejection: IntoResponse;

    fn extract_data(
        request: axum::extract::Request,
        state: &S,
    ) -> impl Future<Output = Result<B, Self::Rejection>> + Send;
}

impl<B: serde::de::DeserializeOwned + Send + 'static, S: Send + Sync + 'static> DataExtractor<B, S>
    for axum::extract::Form<B>
{
    type Rejection = <axum::extract::Form<B> as FromRequest<S>>::Rejection;

    async fn extract_data(
        request: axum::extract::Request,
        state: &S,
    ) -> Result<B, <axum::extract::Form<B> as FromRequest<S>>::Rejection> {
        Self::from_request(request, state).await.map(|form| form.0)
    }
}

impl<B: serde::de::DeserializeOwned + Send + 'static, S: Send + Sync + 'static> DataExtractor<B, S>
    for axum::extract::Json<B>
{
    type Rejection = <axum::extract::Json<B> as FromRequest<S>>::Rejection;

    async fn extract_data(
        request: axum::extract::Request,
        state: &S,
    ) -> Result<B, <axum::extract::Json<B> as FromRequest<S>>::Rejection> {
        Self::from_request(request, state).await.map(|form| form.0)
    }
}

///
/// # Generics
///
/// - H: The handler for the data [DataHandler]
/// - S: The app state
/// - X: The eXtractor used to get the [DataHandler::Body] from the [Request]
///
#[tracing::instrument(skip(handler, state))]
async fn process_data_call<H, S, X>(
    handler: &H,
    request: axum::extract::Request,
    state: &S,
) -> Result<H::Extension, HandlerError<H::Error>>
where
    H: Send + Sync + 'static,
    H: DataHandler<S>,
    S: Send + Sync + Clone + 'static,
    <<H as DataHandler<S>>::Args as FromRequestParts<S>>::Rejection: Send + Sync + 'static,
    X: DataExtractor<<H as DataHandler<S>>::Body, S>,
    H::Body: Send,
{
    let (mut parts, body) = request.into_parts();
    let args = <H::Args as FromRequestParts<S>>::from_request_parts(&mut parts, state).await;

    let args = match args {
        Ok(args) => args,
        Err(error) => {
            let error = crate::Error::wrap_response(error).await;

            tracing::error!(?error, "Unable to extract parts");

            return Err(HandlerError::ParamExtractorRejection(error));
        }
    };
    let parts2 = parts.clone();
    let request = axum::extract::Request::from_parts(parts2, body);
    let body = X::extract_data(request, state).await;
    let body = match body {
        Ok(body) => body,
        Err(error) => {
            let error = crate::Error::wrap_response(error).await;

            tracing::error!(?error, "Unable to extract body");

            return Err(HandlerError::DataExtractorRejection(error));
        }
    };

    handler
        .process_data(args, body)
        .await
        .map_err(HandlerError::DataHandlerError)
}