quokka-handler 0.3.0-beta.0

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

use axum::{
    extract::{FromRequestParts, Request},
    handler::Handler,
    http::StatusCode,
    response::{Html, IntoResponse, Response},
    routing::MethodRouter,
    Extension,
};
use quokka_state::ProvideState;
use quokka_templating::Templating;

use crate::JsonResponse;

#[derive(Clone, Debug)]
pub struct StaticTemplate(pub &'static str);

#[derive(Clone, Debug)]
pub struct StaticTemplateContext(pub serde_json::Value);

///
/// Gets some data via the provided <L: [DataLoader]> and renders it to the template provided in the [Self::path] field.
///
#[derive(Clone, Debug)]
pub struct TemplateRenderer<L> {
    pub path: &'static str,
    _loader: PhantomData<L>,
}

///
/// Gets data via a [DataLoader] and outputs it as a JSON
///
#[derive(Clone, Debug)]
pub struct JsonRenderer<L> {
    _loader: PhantomData<L>,
}

///
/// Give a generic piece of data based on the request parameters. This data might be used together with different handlers like the
/// [crate::FormHandler] or the [crate::JsonHandler] to render either HTML or JSON data. In case it is paired with such a handler and some
/// error happens in this handler, you will have a [axum::extract::Extension]<[crate::HandlerError]> in your [Self::Args] so you can handle
/// the error case
///
pub trait DataLoader<S> {
    type Args: FromRequestParts<S>;
    type Data: serde::Serialize + Send;
    type Error: std::error::Error + Send;

    /// Provide the data that is required to render a certain template
    fn load_data(
        &self,
        params: Self::Args,
    ) -> impl Future<Output = Result<Self::Data, Self::Error>> + Send;

    /// Map an emitted error to a response, this can be used to render another error template instead of the plain error text
    fn render_error(
        &self,
        error: Self::Error,
    ) -> impl Future<Output = impl IntoResponse + Send> + Send {
        async move { error.to_string().into_response() }
    }
}

impl<T, S> Handler<T, S> for StaticTemplate
where
    T: 'static,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
{
    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;

    #[tracing::instrument(skip(request, state))]
    fn call(self, request: Request, state: S) -> Self::Future {
        let template_file = self.0;

        Box::pin(async move {
            let context = request.extensions().get::<StaticTemplateContext>();
            let tpl: Templating = state.provide();

            match tpl.render(template_file, &context.map(|ctx| &ctx.0)) {
                Ok(template) => axum::response::Html(template).into_response(),
                Err(error) => {
                    tracing::error!(?error, "Unable to render static template");

                    let mut response = "Internal server error".into_response();
                    response.extensions_mut().insert(crate::Error::from(error));

                    response
                }
            }
        })
    }
}

impl<S> From<StaticTemplate> for MethodRouter<S>
where
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
{
    fn from(val: StaticTemplate) -> Self {
        MethodRouter::new().get::<_, Response>(val)
    }
}

impl StaticTemplate {
    pub fn into_with_context<S>(self, context: serde_json::Value) -> MethodRouter<S>
    where
        S: Clone + Send + Sync + 'static,
        S: ProvideState<Templating>,
    {
        MethodRouter::new()
            .get::<_, Response>(self)
            .layer(Extension(StaticTemplateContext(context)))
    }
}

impl StaticTemplateContext {
    pub fn new<S: serde::Serialize + 'static>(data: S) -> crate::Result<Self> {
        Ok(Self(serde_json::to_value(data).map_err(
            crate::Error::wrap("Unable to convert static template context data to JSON"),
        )?))
    }
}

impl<L> TemplateRenderer<L> {
    pub fn new(path: &'static str) -> Self {
        Self {
            path,
            _loader: PhantomData,
        }
    }
}

impl<T, S, L> Handler<T, S> for TemplateRenderer<L>
where
    Self: Send,
    T: 'static,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<L>,
    L: DataLoader<S> + Clone + Send + Sync + 'static,
    <L as DataLoader<S>>::Args: Send,
    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
{
    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;

    #[tracing::instrument(skip(self, request, state))]
    fn call(self, request: Request, state: S) -> Self::Future {
        let loader: L = state.provide();

        Box::pin(async move {
            let (mut parts, _body) = request.into_parts();

            data_load_call(&loader, self.path, &mut parts, &state).await
        })
    }
}

impl<S, L> From<TemplateRenderer<L>> for MethodRouter<S>
where
    Self: Send,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<L>,
    L: DataLoader<S> + Clone + Send + Sync + 'static,
    <L as DataLoader<S>>::Args: Send,
    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
{
    fn from(val: TemplateRenderer<L>) -> Self {
        axum::routing::get::<_, Response, _>(val)
    }
}

impl<L> Default for JsonRenderer<L> {
    fn default() -> Self {
        Self::new()
    }
}

impl<L> JsonRenderer<L> {
    pub fn new() -> Self {
        Self {
            _loader: PhantomData,
        }
    }
}

impl<T, S, L> Handler<T, S> for JsonRenderer<L>
where
    Self: Send,
    T: 'static,
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<L>,
    L: DataLoader<S> + Clone + Send + Sync + 'static,
    <L as DataLoader<S>>::Args: Send,
    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
{
    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;

    #[tracing::instrument(skip(self, request, state))]
    fn call(self, request: Request, state: S) -> Self::Future {
        let loader: L = state.provide();

        Box::pin(async move {
            let (mut parts, _body) = request.into_parts();
            let Ok(params) = L::Args::from_request_parts(&mut parts, &state)
                .await
                .inspect_err(|_| {
                    // TODO: Figure out how to properly debug the axum Rejections
                    tracing::error!(
                        loader = std::any::type_name::<L>(),
                        "Unable to extract Loader::Args in DataJsonRendrer for Loader"
                    )
                })
            else {
                return JsonResponse::<(), ()>::error(
                    500,
                    "Internal Server Error",
                    "Unable to load data",
                )
                .into_response();
            };

            loader
                .load_data(params)
                .await
                .inspect_err(|error| {
                    tracing::error!(
                        ?error,
                        loader = std::any::type_name::<L>(),
                        "Unable to load data from DataLoader"
                    )
                })
                .map(JsonResponse::data)
                .map_err(|_| {
                    JsonResponse::<(), ()>::error(
                        500,
                        "Internal Server error",
                        "Unable to load data",
                    )
                })
                .into_response()
        })
    }
}

impl<S, L> From<JsonRenderer<L>> for MethodRouter<S>
where
    S: Clone + Send + Sync + 'static,
    S: ProvideState<Templating>,
    S: ProvideState<L>,
    L: DataLoader<S> + Clone + Send + Sync + 'static,
    <L as DataLoader<S>>::Args: Send,
    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
{
    fn from(val: JsonRenderer<L>) -> Self {
        MethodRouter::new().get::<_, Response>(val)
    }
}

#[tracing::instrument(skip(state, loader))]
async fn data_load_call<S, L>(
    loader: &L,
    template: &'static str,
    parts: &mut axum::http::request::Parts,
    state: &S,
) -> axum::response::Response
where
    L: Send + Sync,
    L::Args: Send,
    S: Send + Sync,
    S: ProvideState<Templating>,
    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
    L: DataLoader<S>,
{
    let templating: Templating = state.provide();
    let args = <L::Args as FromRequestParts<S>>::from_request_parts(parts, state).await;

    let args = match args {
        Ok(args) => args,
        Err(error) => {
            let mut response = (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Unable to process request",
            )
                .into_response();

            let error = crate::Error::wrap_response(error).await;

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

            response.extensions_mut().insert(error);

            return response;
        }
    };

    let data = match loader.load_data(args).await {
        Ok(data) => data,
        Err(error) => {
            return loader.render_error(error).await.into_response();
        }
    };

    let render = match templating.render(template, &data) {
        Ok(render) => render,
        Err(error) => {
            let mut response = (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Unable to process request",
            )
                .into_response();

            tracing::error!(?error, "Unable to render template");

            response.extensions_mut().insert(error);

            return response;
        }
    };

    Html(render).into_response()
}