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 UiReqInfoUnchecked {
47 pub vhost: ui_vhost::UiResult,
48 pub send_format: SendFormat,
49}
50
51pub struct UiReqInfo {
52 pub vhost: ui_vhost::IsUi,
53 pub send_format: SendFormat,
54}
55
56pub enum SendFormat {
57 Html,
58 Json,
59}
60
61impl UiReqInfoUnchecked {
62 pub fn check(self) -> Result<UiReqInfo, WebError> {
63 let UiReqInfoUnchecked { vhost, send_format } = self;
64 let vhost = vhost.check(WE::PageNotFoundHere)?;
65 Ok(UiReqInfo { vhost, send_format })
66 }
67}
68
69#[async_trait]
70impl<'r> FromRequest<'r> for SendFormat {
71 type Error = String;
72
73 async fn from_request(req: &'r Request<'_>) -> Outcome<Self, String> {
74 FromRequest::from_request(req).await.map(|a: &rocket::http::Accept| {
75 if a.preferred().is_json() {
76 SendFormat::Json
77 } else {
78 SendFormat::Html
79 }
80 }).map_error(|(_, inf)| match inf {})
81 }
82}
83
84macro_rules! load_template_parts { { $( $name:literal ),* $(,)? } => { $(
85 inventory::submit!(EmbeddedTemplate {
86 name: $name,
87 contents: include_str!(concat!("../ui/", $name)),
88 is_part: Some(EmbeddedTemplateIsJustAPart),
89 });
90)* } }
91
92#[macro_export]
117macro_rules! template { {
118 $name:literal, $req_info:expr;
119 $( content_type: $ctype:expr; )?
120 { $($context_always:tt)* }
121 $( { $($context_html_only:tt)* } )?
122} => {
123 $crate::template! {
124 $name, $req_info;
125 $( $content_type: $ctype )?
126 ($crate::tera_context!( $( $context_always )* ),
127 $crate::tera_context!( $( $( $context_html_only )* )? ))
128 }
129};
130{
131 $name:literal, $req_info:expr;
132 $( content_type: $ctype:expr; )?
133 ( $context_always:expr, $context_html_only:expr )
134 $(,)?
135} => {
136 {
137 use $crate::ui_render::*;
138
139 let _: $crate::ui_vhost::IsUi = $req_info.vhost;
140
141 match $req_info.send_format {
142 SendFormat::Html => {}
143 SendFormat::Json => {
144 let json = $context_always.into_json();
145 let json = serde_json::to_string(&json)
146 .with_context(|| format!("render json for {}", $name))
147 .map_err(IE::new_quiet)
148 .map_err(WebError::from)?;
149 return Ok((ContentType::JSON, json));
150 }
151 }
152 let mut context = $context_always;
153 context.extend($context_html_only);
154
155 #[allow(dead_code)]
156 let content_type = $name.rsplit_once('.')
157 .and_then(|(_, ext)| ContentType::from_extension(ext));
158 $(
159 let content_type: ContentType = Some($ctype);
160 )?
161 let content_type = content_type
162 .ok_or_else(|| WebError::from(IE::new_quiet(anyhow!(
163 "cannot deduce Content-Type for template {}", $name
164 ))))?;
165
166 let s = $crate::template_html_unchecked! { $name, context }?;
167 Ok((content_type, s))
168 }
169} }
170
171#[macro_export]
177macro_rules! template_html_unchecked { {
178 $name:literal, $context:expr $(,)?
179} => {
180 {
181 let gl = globals();
182
183 inventory::submit!(EmbeddedTemplate {
184 name: $name,
185 contents: include_str!(concat!("../ui/", $name)),
186 is_part: None,
187 });
188
189 #[cfg(test)]
190 gl.t_note_template_rendered($name);
191
192 let s = gl.tera.render(
193 $name,
194 &$context,
195 )
196 .context($name)
197 .map_err(IE::new_quiet)
198 .map_err(WebError::from)?;
199
200 Ok::<_, WebError>(s)
201 }
202} }
203
204#[macro_export]
211macro_rules! template_page { {
212 $name:literal, $req_info:expr;
213 { $($context:tt)* }
214} => {
215 $crate::template! {
216 $name, $req_info;
217 {
218 $($context)*
219 t2usm_version: &globals().version_info,
220 }
221 {
222 navbar: $crate::ui_render::make_navbar_for($name, NAVBAR),
223 t2usm_version: &globals().version_info.to_string(),
224 }
225 }
226} }
227
228#[macro_export]
240macro_rules! tera_context { {
241 $( $k:ident $( : $v:expr )? ),* $(,)?
242} => { {
243 #[allow(unused_mut)]
244 let mut context = tera::Context::new();
245 $( $crate::tera_context!( @ context $k $( $v )? ); )*
246 context
247} }; {
248 @ $context:ident $k:ident
249} => {
250 $crate::tera_context!(@ $context $k $k)
251}; {
252 @ $context:ident $k:ident $v:expr
253} => {
254 $context.insert(stringify!($k), &$v)
255} }
256
257pub fn tera_templates(config: &Config) -> Result<Tera, StartupError> {
258 if let Some(dir) = &config.files.template_dir {
259 let glob = format!("{dir}/*[^~#]");
260 debug!(?glob, "loading tera templates");
261 Tera::new(&glob)
262 .context(glob)
263 .map_err(StartupError::Templates)
264 } else {
265 embedded_tera_templates()
266 }
267}
268
269pub fn embedded_tera_templates() -> Result<Tera, StartupError> {
270 let mut tera = Tera::default();
271
272 tera.add_raw_templates(
273 inventory::iter::<EmbeddedTemplate>().map(
274 |EmbeddedTemplate { name, contents, is_part: _ }| {
275 trace!(name, "loading builtin templat");
276 (name, contents)
277 }
278 )
279 ).into_internal("failed to initialise templating")?;
280
281 Ok(tera)
282}
283
284pub type NavbarEntry<'s> = (&'s str, &'s str, &'s str);
286
287pub fn make_navbar_for(for_templ: &str, navbar: &[NavbarEntry]) -> String {
288 let mut out = String::new();
289 let mut delim = "";
290 for (title, templ, path) in navbar.iter().copied() {
291 write_string!(out, "{}", mem::replace(&mut delim, " | "));
292 let post = if templ != for_templ {
293 write_string!(out, "<a href={path}>");
294 "</a>"
295 } else {
296 ""
297 };
298 write_string!(out, "<b>{title}</b>{post}");
299 }
300 out
301}
302
303inventory::collect!(EmbeddedTemplate);
304
305#[test]
306fn check_embedded_tera_templates() {
307 let t: Tera = embedded_tera_templates().expect("bad templates?");
308 println!("{t:?}");
309}