tag2upload_service_manager/
ui_render.rs1use crate::prelude::*;
7
8pub use tera::Tera;
9pub use rocket::http::ContentType;
10
11use rocket::request::{FromRequest, Outcome, Request};
12
13pub type RenderedTemplate = Result<(ContentType, String), WebError>;
14
15pub struct EmbeddedTemplateIsJustAPart;
16
17pub struct EmbeddedTemplate {
18 pub name: &'static str,
19 pub contents: &'static str,
20 pub is_part: Option<EmbeddedTemplateIsJustAPart>,
21}
22
23define_derive_deftly! {
24 FromRequest for struct, expect items:
25
26 #[async_trait]
27 impl<'r> FromRequest<'r> for $ttype {
28 type Error = String;
29
30 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, String> {
31 use Outcome as O;
32
33 O::Success($tname { $(
34 $fname: match FromRequest::from_request(req).await {
35 O::Success(y) => y,
36 O::Forward(x) => return O::Forward(x),
37 O::Error(e) => return O::Error(e),
38 },
39 ) })
40 }
41 }
42}
43
44#[derive(Deftly)]
45#[derive_deftly(FromRequest)]
46pub struct UiReqInfo {
47 pub vhost: ui_vhost::UiResult,
48 pub send_format: SendFormat,
49}
50
51pub enum SendFormat {
52 Html,
53 Json,
54}
55
56#[async_trait]
57impl<'r> FromRequest<'r> for SendFormat {
58 type Error = String;
59
60 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, String> {
61 FromRequest::from_request(req).await.map(|a: &rocket::http::Accept| {
62 if a.preferred().is_json() {
63 SendFormat::Json
64 } else {
65 SendFormat::Html
66 }
67 }).map_error(|(_, inf)| match inf {})
68 }
69}
70
71macro_rules! load_template_parts { { $( $name:literal ),* $(,)? } => { $(
72 inventory::submit!(EmbeddedTemplate {
73 name: $name,
74 contents: include_str!(concat!("../ui/", $name)),
75 is_part: Some(EmbeddedTemplateIsJustAPart),
76 });
77)* } }
78
79#[macro_export]
104macro_rules! template { {
105 $name:literal, $req_info:expr;
106 $( content_type: $ctype:expr; )?
107 { $($context_always:tt)* }
108 $( { $($context_html_only:tt)* } )?
109} => {
110 $crate::template! {
111 $name, $req_info;
112 $( $content_type: $ctype )?
113 ($crate::tera_context!( $( $context_always )* ),
114 $crate::tera_context!( $( $( $context_html_only )* )? ))
115 }
116};
117{
118 $name:literal, $req_info:expr;
119 $( content_type: $ctype:expr; )?
120 ( $context_always:expr, $context_html_only:expr )
121 $(,)?
122} => {
123 {
124 use $crate::ui_render::*;
125
126 let _: $crate::ui_vhost::IsUi =
127 $req_info.vhost.check(WE::PageNotFoundHere)?;
128
129 match $req_info.send_format {
130 SendFormat::Html => {}
131 SendFormat::Json => {
132 let json = $context_always.into_json();
133 let json = serde_json::to_string(&json)
134 .with_context(|| format!("render json for {}", $name))
135 .map_err(IE::new_quiet)
136 .map_err(WebError::from)?;
137 return Ok((ContentType::JSON, json));
138 }
139 }
140 let mut context = $context_always;
141 context.extend($context_html_only);
142
143 #[allow(dead_code)]
144 let content_type = $name.rsplit_once('.')
145 .and_then(|(_, ext)| ContentType::from_extension(ext));
146 $(
147 let content_type: ContentType = Some($ctype);
148 )?
149 let content_type = content_type
150 .ok_or_else(|| WebError::from(IE::new_quiet(anyhow!(
151 "cannot deduce Content-Type for template {}", $name
152 ))))?;
153
154 let s = $crate::template_html_unchecked! { $name, context }?;
155 Ok((content_type, s))
156 }
157} }
158
159#[macro_export]
165macro_rules! template_html_unchecked { {
166 $name:literal, $context:expr $(,)?
167} => {
168 {
169 let gl = globals();
170
171 inventory::submit!(EmbeddedTemplate {
172 name: $name,
173 contents: include_str!(concat!("../ui/", $name)),
174 is_part: None,
175 });
176
177 #[cfg(test)]
178 gl.t_note_template_rendered($name);
179
180 let s = gl.tera.render(
181 $name,
182 &$context,
183 )
184 .context($name)
185 .map_err(IE::new_quiet)
186 .map_err(WebError::from)?;
187
188 Ok::<_, WebError>(s)
189 }
190} }
191
192#[macro_export]
199macro_rules! template_page { {
200 $name:literal, $req_info:expr;
201 { $($context:tt)* }
202} => {
203 $crate::template! {
204 $name, $req_info;
205 {
206 $($context)*
207 t2usm_version: &globals().version_info,
208 }
209 {
210 navbar: $crate::ui_render::make_navbar_for($name, NAVBAR),
211 t2usm_version: &globals().version_info.to_string(),
212 }
213 }
214} }
215
216#[macro_export]
228macro_rules! tera_context { {
229 $( $k:ident $( : $v:expr )? ),* $(,)?
230} => { {
231 #[allow(unused_mut)]
232 let mut context = tera::Context::new();
233 $( $crate::tera_context!( @ context $k $( $v )? ); )*
234 context
235} }; {
236 @ $context:ident $k:ident
237} => {
238 $crate::tera_context!(@ $context $k $k)
239}; {
240 @ $context:ident $k:ident $v:expr
241} => {
242 $context.insert(stringify!($k), &$v)
243} }
244
245pub fn tera_templates(config: &Config) -> Result<Tera, StartupError> {
246 if let Some(dir) = &config.files.template_dir {
247 let glob = format!("{dir}/*[^~#]");
248 debug!(?glob, "loading tera templates");
249 Tera::new(&glob)
250 .context(glob)
251 .map_err(StartupError::Templates)
252 } else {
253 embedded_tera_templates()
254 }
255}
256
257pub fn embedded_tera_templates() -> Result<Tera, StartupError> {
258 let mut tera = Tera::default();
259
260 tera.add_raw_templates(
261 inventory::iter::<EmbeddedTemplate>().map(
262 |EmbeddedTemplate { name, contents, is_part: _ }| {
263 trace!(name, "loading builtin templat");
264 (name, contents)
265 }
266 )
267 ).into_internal("failed to initialise templating")?;
268
269 Ok(tera)
270}
271
272pub type NavbarEntry<'s> = (&'s str, &'s str, &'s str);
274
275pub fn make_navbar_for(for_templ: &str, navbar: &[NavbarEntry]) -> String {
276 let mut out = String::new();
277 let mut delim = "";
278 for (title, templ, path) in navbar.iter().copied() {
279 write_string!(out, "{}", mem::replace(&mut delim, " | "));
280 let post = if templ != for_templ {
281 write_string!(out, "<a href={path}>");
282 "</a>"
283 } else {
284 ""
285 };
286 write_string!(out, "<b>{title}</b>{post}");
287 }
288 out
289}
290
291inventory::collect!(EmbeddedTemplate);
292
293#[test]
294fn check_embedded_tera_templates() {
295 let t: Tera = embedded_tera_templates().expect("bad templates?");
296 println!("{t:?}");
297}