Skip to main content

quokka_handler/
render.rs

1use std::{future::Future, marker::PhantomData, pin::Pin};
2
3use axum::{
4    extract::{FromRequestParts, Request},
5    handler::Handler,
6    http::StatusCode,
7    response::{Html, IntoResponse, Response},
8    routing::MethodRouter,
9    Extension,
10};
11use quokka_state::ProvideState;
12use quokka_templating::Templating;
13
14use crate::JsonResponse;
15
16#[derive(Clone, Debug)]
17pub struct StaticTemplate(pub &'static str);
18
19#[derive(Clone, Debug)]
20pub struct StaticTemplateContext(pub serde_json::Value);
21
22///
23/// Gets some data via the provided <L: [DataLoader]> and renders it to the template provided in the [Self::path] field.
24///
25#[derive(Clone, Debug)]
26pub struct TemplateRenderer<L> {
27    pub path: &'static str,
28    _loader: PhantomData<L>,
29}
30
31///
32/// Gets data via a [DataLoader] and outputs it as a JSON
33///
34#[derive(Clone, Debug)]
35pub struct JsonRenderer<L> {
36    _loader: PhantomData<L>,
37}
38
39///
40/// Give a generic piece of data based on the request parameters. This data might be used together with different handlers like the
41/// [crate::FormHandler] or the [crate::JsonHandler] to render either HTML or JSON data. In case it is paired with such a handler and some
42/// error happens in this handler, you will have a [axum::extract::Extension]<[crate::HandlerError]> in your [Self::Args] so you can handle
43/// the error case
44///
45pub trait DataLoader<S> {
46    type Args: FromRequestParts<S>;
47    type Data: serde::Serialize + Send;
48    type Error: std::error::Error + Send;
49
50    /// Provide the data that is required to render a certain template
51    fn load_data(
52        &self,
53        params: Self::Args,
54    ) -> impl Future<Output = Result<Self::Data, Self::Error>> + Send;
55
56    /// Map an emitted error to a response, this can be used to render another error template instead of the plain error text
57    fn render_error(
58        &self,
59        error: Self::Error,
60    ) -> impl Future<Output = impl IntoResponse + Send> + Send {
61        async move { error.to_string().into_response() }
62    }
63}
64
65impl<T, S> Handler<T, S> for StaticTemplate
66where
67    T: 'static,
68    S: Clone + Send + Sync + 'static,
69    S: ProvideState<Templating>,
70{
71    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;
72
73    #[tracing::instrument(skip(request, state))]
74    fn call(self, request: Request, state: S) -> Self::Future {
75        let template_file = self.0;
76
77        Box::pin(async move {
78            let context = request.extensions().get::<StaticTemplateContext>();
79            let tpl: Templating = state.provide();
80
81            match tpl.render(template_file, &context.map(|ctx| &ctx.0)) {
82                Ok(template) => axum::response::Html(template).into_response(),
83                Err(error) => {
84                    tracing::error!(?error, "Unable to render static template");
85
86                    let mut response = "Internal server error".into_response();
87                    response.extensions_mut().insert(crate::Error::from(error));
88
89                    response
90                }
91            }
92        })
93    }
94}
95
96impl<S> From<StaticTemplate> for MethodRouter<S>
97where
98    S: Clone + Send + Sync + 'static,
99    S: ProvideState<Templating>,
100{
101    fn from(val: StaticTemplate) -> Self {
102        MethodRouter::new().get::<_, Response>(val)
103    }
104}
105
106impl StaticTemplate {
107    pub fn into_with_context<S>(self, context: serde_json::Value) -> MethodRouter<S>
108    where
109        S: Clone + Send + Sync + 'static,
110        S: ProvideState<Templating>,
111    {
112        MethodRouter::new()
113            .get::<_, Response>(self)
114            .layer(Extension(StaticTemplateContext(context)))
115    }
116}
117
118impl StaticTemplateContext {
119    pub fn new<S: serde::Serialize + 'static>(data: S) -> crate::Result<Self> {
120        Ok(Self(serde_json::to_value(data).map_err(
121            crate::Error::wrap("Unable to convert static template context data to JSON"),
122        )?))
123    }
124}
125
126impl<L> TemplateRenderer<L> {
127    pub fn new(path: &'static str) -> Self {
128        Self {
129            path,
130            _loader: PhantomData,
131        }
132    }
133}
134
135impl<T, S, L> Handler<T, S> for TemplateRenderer<L>
136where
137    Self: Send,
138    T: 'static,
139    S: Clone + Send + Sync + 'static,
140    S: ProvideState<Templating>,
141    S: ProvideState<L>,
142    L: DataLoader<S> + Clone + Send + Sync + 'static,
143    <L as DataLoader<S>>::Args: Send,
144    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
145{
146    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;
147
148    #[tracing::instrument(skip(self, request, state))]
149    fn call(self, request: Request, state: S) -> Self::Future {
150        let loader: L = state.provide();
151
152        Box::pin(async move {
153            let (mut parts, _body) = request.into_parts();
154
155            data_load_call(&loader, self.path, &mut parts, &state).await
156        })
157    }
158}
159
160impl<S, L> From<TemplateRenderer<L>> for MethodRouter<S>
161where
162    Self: Send,
163    S: Clone + Send + Sync + 'static,
164    S: ProvideState<Templating>,
165    S: ProvideState<L>,
166    L: DataLoader<S> + Clone + Send + Sync + 'static,
167    <L as DataLoader<S>>::Args: Send,
168    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
169{
170    fn from(val: TemplateRenderer<L>) -> Self {
171        axum::routing::get::<_, Response, _>(val)
172    }
173}
174
175impl<L> Default for JsonRenderer<L> {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181impl<L> JsonRenderer<L> {
182    pub fn new() -> Self {
183        Self {
184            _loader: PhantomData,
185        }
186    }
187}
188
189impl<T, S, L> Handler<T, S> for JsonRenderer<L>
190where
191    Self: Send,
192    T: 'static,
193    S: Clone + Send + Sync + 'static,
194    S: ProvideState<Templating>,
195    S: ProvideState<L>,
196    L: DataLoader<S> + Clone + Send + Sync + 'static,
197    <L as DataLoader<S>>::Args: Send,
198    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
199{
200    type Future = Pin<Box<dyn Future<Output = axum::response::Response> + Send>>;
201
202    #[tracing::instrument(skip(self, request, state))]
203    fn call(self, request: Request, state: S) -> Self::Future {
204        let loader: L = state.provide();
205
206        Box::pin(async move {
207            let (mut parts, _body) = request.into_parts();
208            let Ok(params) = L::Args::from_request_parts(&mut parts, &state)
209                .await
210                .inspect_err(|_| {
211                    // TODO: Figure out how to properly debug the axum Rejections
212                    tracing::error!(
213                        loader = std::any::type_name::<L>(),
214                        "Unable to extract Loader::Args in DataJsonRendrer for Loader"
215                    )
216                })
217            else {
218                return JsonResponse::<(), ()>::error(
219                    500,
220                    "Internal Server Error",
221                    "Unable to load data",
222                )
223                .into_response();
224            };
225
226            loader
227                .load_data(params)
228                .await
229                .inspect_err(|error| {
230                    tracing::error!(
231                        ?error,
232                        loader = std::any::type_name::<L>(),
233                        "Unable to load data from DataLoader"
234                    )
235                })
236                .map(JsonResponse::data)
237                .map_err(|_| {
238                    JsonResponse::<(), ()>::error(
239                        500,
240                        "Internal Server error",
241                        "Unable to load data",
242                    )
243                })
244                .into_response()
245        })
246    }
247}
248
249impl<S, L> From<JsonRenderer<L>> for MethodRouter<S>
250where
251    S: Clone + Send + Sync + 'static,
252    S: ProvideState<Templating>,
253    S: ProvideState<L>,
254    L: DataLoader<S> + Clone + Send + Sync + 'static,
255    <L as DataLoader<S>>::Args: Send,
256    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
257{
258    fn from(val: JsonRenderer<L>) -> Self {
259        MethodRouter::new().get::<_, Response>(val)
260    }
261}
262
263#[tracing::instrument(skip(state, loader))]
264async fn data_load_call<S, L>(
265    loader: &L,
266    template: &'static str,
267    parts: &mut axum::http::request::Parts,
268    state: &S,
269) -> axum::response::Response
270where
271    L: Send + Sync,
272    L::Args: Send,
273    S: Send + Sync,
274    S: ProvideState<Templating>,
275    <<L as DataLoader<S>>::Args as FromRequestParts<S>>::Rejection: Send,
276    L: DataLoader<S>,
277{
278    let templating: Templating = state.provide();
279    let args = <L::Args as FromRequestParts<S>>::from_request_parts(parts, state).await;
280
281    let args = match args {
282        Ok(args) => args,
283        Err(error) => {
284            let mut response = (
285                StatusCode::INTERNAL_SERVER_ERROR,
286                "Unable to process request",
287            )
288                .into_response();
289
290            let error = crate::Error::wrap_response(error).await;
291
292            tracing::error!(?error, "Unable to extract parts");
293
294            response.extensions_mut().insert(error);
295
296            return response;
297        }
298    };
299
300    let data = match loader.load_data(args).await {
301        Ok(data) => data,
302        Err(error) => {
303            return loader.render_error(error).await.into_response();
304        }
305    };
306
307    let render = match templating.render(template, &data) {
308        Ok(render) => render,
309        Err(error) => {
310            let mut response = (
311                StatusCode::INTERNAL_SERVER_ERROR,
312                "Unable to process request",
313            )
314                .into_response();
315
316            tracing::error!(?error, "Unable to render template");
317
318            response.extensions_mut().insert(error);
319
320            return response;
321        }
322    };
323
324    Html(render).into_response()
325}