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#[derive(Clone, Debug)]
26pub struct TemplateRenderer<L> {
27 pub path: &'static str,
28 _loader: PhantomData<L>,
29}
30
31#[derive(Clone, Debug)]
35pub struct JsonRenderer<L> {
36 _loader: PhantomData<L>,
37}
38
39pub trait DataLoader<S> {
46 type Args: FromRequestParts<S>;
47 type Data: serde::Serialize + Send;
48 type Error: std::error::Error + Send;
49
50 fn load_data(
52 &self,
53 params: Self::Args,
54 ) -> impl Future<Output = Result<Self::Data, Self::Error>> + Send;
55
56 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 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}