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);
#[derive(Clone, Debug)]
pub struct TemplateRenderer<L> {
pub path: &'static str,
_loader: PhantomData<L>,
}
#[derive(Clone, Debug)]
pub struct JsonRenderer<L> {
_loader: PhantomData<L>,
}
pub trait DataLoader<S> {
type Args: FromRequestParts<S>;
type Data: serde::Serialize + Send;
type Error: std::error::Error + Send;
fn load_data(
&self,
params: Self::Args,
) -> impl Future<Output = Result<Self::Data, Self::Error>> + Send;
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(|_| {
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()
}