rocket_dyn_templates/template.rs
1use std::borrow::Cow;
2use std::path::PathBuf;
3
4use rocket::{Rocket, Orbit, Ignite, Sentinel};
5use rocket::request::Request;
6use rocket::fairing::Fairing;
7use rocket::response::{self, Responder};
8use rocket::http::{ContentType, Status};
9use rocket::figment::{value::Value, error::Error};
10use rocket::serde::Serialize;
11use rocket::yansi::Paint;
12
13use crate::Engines;
14use crate::fairing::TemplateFairing;
15use crate::context::{Context, ContextManager};
16
17pub(crate) const DEFAULT_TEMPLATE_DIR: &str = "templates";
18
19/// Responder that renders a dynamic template.
20///
21/// `Template` serves as a _proxy_ type for rendering a template and _does not_
22/// contain the rendered template itself. The template is lazily rendered, at
23/// response time. To render a template greedily, use [`Template::show()`].
24///
25/// See the [crate root](crate) for usage details.
26#[derive(Debug)]
27pub struct Template {
28 name: Cow<'static, str>,
29 value: Result<Value, Error>,
30}
31
32#[derive(Debug)]
33pub(crate) struct TemplateInfo {
34 /// The complete path, including `template_dir`, to this template, if any.
35 pub(crate) path: Option<PathBuf>,
36 /// The extension for the engine of this template.
37 pub(crate) engine_ext: &'static str,
38 /// The extension before the engine extension in the template, if any.
39 pub(crate) data_type: ContentType
40}
41
42impl Template {
43 /// Returns a fairing that initializes and maintains templating state.
44 ///
45 /// This fairing, or the one returned by [`Template::custom()`], _must_ be
46 /// attached to any `Rocket` instance that wishes to render templates.
47 /// Failure to attach this fairing will result in a "Uninitialized template
48 /// context: missing fairing." error message when a template is attempted to
49 /// be rendered.
50 ///
51 /// If you wish to customize the internal templating engines, use
52 /// [`Template::custom()`] instead.
53 ///
54 /// # Example
55 ///
56 /// To attach this fairing, simple call `attach` on the application's
57 /// `Rocket` instance with `Template::fairing()`:
58 ///
59 /// ```rust
60 /// extern crate rocket;
61 /// extern crate rocket_dyn_templates;
62 ///
63 /// use rocket_dyn_templates::Template;
64 ///
65 /// fn main() {
66 /// rocket::build()
67 /// // ...
68 /// .attach(Template::fairing())
69 /// // ...
70 /// # ;
71 /// }
72 /// ```
73 pub fn fairing() -> impl Fairing {
74 Template::custom(|_| {})
75 }
76
77 /// Returns a fairing that initializes and maintains templating state.
78 ///
79 /// Unlike [`Template::fairing()`], this method allows you to configure
80 /// templating engines via the function `f`. Note that only the enabled
81 /// templating engines will be accessible from the `Engines` type.
82 ///
83 /// This method does not allow the function `f` to fail. If `f` is fallible,
84 /// use [`Template::try_custom()`] instead.
85 ///
86 /// # Example
87 ///
88 /// ```rust
89 /// extern crate rocket;
90 /// extern crate rocket_dyn_templates;
91 ///
92 /// use rocket_dyn_templates::Template;
93 ///
94 /// fn main() {
95 /// rocket::build()
96 /// // ...
97 /// .attach(Template::custom(|engines| {
98 /// // engines.handlebars.register_helper ...
99 /// }))
100 /// // ...
101 /// # ;
102 /// }
103 /// ```
104 pub fn custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
105 where F: Fn(&mut Engines)
106 {
107 Self::try_custom(move |engines| { f(engines); Ok(()) })
108 }
109
110 /// Returns a fairing that initializes and maintains templating state.
111 ///
112 /// This variant of [`Template::custom()`] allows a fallible `f`. If `f`
113 /// returns an error during initialization, it will cancel the launch. If
114 /// `f` returns an error during template reloading (in debug mode), then the
115 /// newly-reloaded templates are discarded.
116 ///
117 /// # Example
118 ///
119 /// ```rust
120 /// extern crate rocket;
121 /// extern crate rocket_dyn_templates;
122 ///
123 /// use rocket_dyn_templates::Template;
124 ///
125 /// fn main() {
126 /// rocket::build()
127 /// // ...
128 /// .attach(Template::try_custom(|engines| {
129 /// // engines.handlebars.register_helper ...
130 /// Ok(())
131 /// }))
132 /// // ...
133 /// # ;
134 /// }
135 /// ```
136 pub fn try_custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
137 where F: Fn(&mut Engines) -> Result<(), Box<dyn std::error::Error>>
138 {
139 TemplateFairing { callback: Box::new(f) }
140 }
141
142 /// Render the template named `name` with the context `context`. The
143 /// `context` is typically created using the [`context!`] macro, but it can
144 /// be of any type that implements `Serialize`, such as `HashMap` or a
145 /// custom `struct`.
146 ///
147 /// To render a template directly into a string, use [`Metadata::render()`].
148 ///
149 /// # Examples
150 ///
151 /// Using the `context` macro:
152 ///
153 /// ```rust
154 /// use rocket_dyn_templates::{Template, context};
155 ///
156 /// let template = Template::render("index", context! {
157 /// foo: "Hello, world!",
158 /// });
159 /// ```
160 ///
161 /// Using a `HashMap` as the context:
162 ///
163 /// ```rust
164 /// use std::collections::HashMap;
165 /// use rocket_dyn_templates::Template;
166 ///
167 /// // Create a `context` from a `HashMap`.
168 /// let mut context = HashMap::new();
169 /// context.insert("foo", "Hello, world!");
170 ///
171 /// let template = Template::render("index", context);
172 /// ```
173 #[inline]
174 pub fn render<S, C>(name: S, context: C) -> Template
175 where S: Into<Cow<'static, str>>, C: Serialize
176 {
177 Template {
178 name: name.into(),
179 value: Value::serialize(context),
180 }
181 }
182
183 /// Render the template named `name` with the context `context` into a
184 /// `String`. This method should **not** be used in any running Rocket
185 /// application. This method should only be used during testing to validate
186 /// `Template` responses. For other uses, use [`render()`](#method.render)
187 /// instead.
188 ///
189 /// The `context` can be of any type that implements `Serialize`. This is
190 /// typically a `HashMap` or a custom `struct`.
191 ///
192 /// Returns `Some` if the template could be rendered. Otherwise, returns
193 /// `None`. If rendering fails, error output is printed to the console.
194 /// `None` is also returned if a `Template` fairing has not been attached.
195 ///
196 /// # Example
197 ///
198 /// ```rust,no_run
199 /// # extern crate rocket;
200 /// # extern crate rocket_dyn_templates;
201 /// use std::collections::HashMap;
202 ///
203 /// use rocket_dyn_templates::Template;
204 /// use rocket::local::blocking::Client;
205 ///
206 /// fn main() {
207 /// let rocket = rocket::build().attach(Template::fairing());
208 /// let client = Client::untracked(rocket).expect("valid rocket");
209 ///
210 /// // Create a `context`. Here, just an empty `HashMap`.
211 /// let mut context = HashMap::new();
212 /// # context.insert("test", "test");
213 /// let template = Template::show(client.rocket(), "index", context);
214 /// }
215 /// ```
216 #[inline]
217 pub fn show<S, C>(rocket: &Rocket<Orbit>, name: S, context: C) -> Option<String>
218 where S: Into<Cow<'static, str>>, C: Serialize
219 {
220 let ctxt = rocket.state::<ContextManager>().map(ContextManager::context).or_else(|| {
221 warn!("Uninitialized template context: missing fairing.");
222 info!("To use templates, you must attach `Template::fairing()`.");
223 info!("See the `Template` documentation for more information.");
224 None
225 })?;
226
227 Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1)
228 }
229
230 /// Actually render this template given a template context. This method is
231 /// called by the `Template` `Responder` implementation as well as
232 /// `Template::show()`.
233 #[inline(always)]
234 pub(crate) fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> {
235 let name = &*self.name;
236 let info = ctxt.templates.get(name).ok_or_else(|| {
237 let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
238 error_!("Template '{}' does not exist.", name);
239 info_!("Known templates: {}.", ts.join(", "));
240 info_!("Searched in {:?}.", ctxt.root);
241 Status::InternalServerError
242 })?;
243
244 let value = self.value.map_err(|e| {
245 error_!("Template context failed to serialize: {}.", e);
246 Status::InternalServerError
247 })?;
248
249 let string = ctxt.engines.render(name, info, value).ok_or_else(|| {
250 error_!("Template '{}' failed to render.", name);
251 Status::InternalServerError
252 })?;
253
254 Ok((info.data_type.clone(), string))
255 }
256}
257
258/// Returns a response with the Content-Type derived from the template's
259/// extension and a fixed-size body containing the rendered template. If
260/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
261impl<'r> Responder<'r, 'static> for Template {
262 fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
263 let ctxt = req.rocket()
264 .state::<ContextManager>()
265 .ok_or_else(|| {
266 error_!("Uninitialized template context: missing fairing.");
267 info_!("To use templates, you must attach `Template::fairing()`.");
268 info_!("See the `Template` documentation for more information.");
269 Status::InternalServerError
270 })?;
271
272 self.finalize(&ctxt.context())?.respond_to(req)
273 }
274}
275
276impl Sentinel for Template {
277 fn abort(rocket: &Rocket<Ignite>) -> bool {
278 if rocket.state::<ContextManager>().is_none() {
279 let template = "Template".primary().bold();
280 let fairing = "Template::fairing()".primary().bold();
281 error!("returning `{}` responder without attaching `{}`.", template, fairing);
282 info_!("To use or query templates, you must attach `{}`.", fairing);
283 info_!("See the `Template` documentation for more information.");
284 return true;
285 }
286
287 false
288 }
289}
290
291/// A macro to easily create a template rendering context.
292///
293/// Invocations of this macro expand to a value of an anonymous type which
294/// implements [`serde::Serialize`]. Fields can be literal expressions or
295/// variables captured from a surrounding scope, as long as all fields implement
296/// `Serialize`.
297///
298/// # Examples
299///
300/// The following code:
301///
302/// ```rust
303/// # #[macro_use] extern crate rocket;
304/// # use rocket_dyn_templates::{Template, context};
305/// #[get("/<foo>")]
306/// fn render_index(foo: u64) -> Template {
307/// Template::render("index", context! {
308/// // Note that shorthand field syntax is supported.
309/// // This is equivalent to `foo: foo,`
310/// foo,
311/// bar: "Hello world",
312/// })
313/// }
314/// ```
315///
316/// is equivalent to the following, but without the need to manually define an
317/// `IndexContext` struct:
318///
319/// ```rust
320/// # use rocket_dyn_templates::Template;
321/// # use rocket::serde::Serialize;
322/// # use rocket::get;
323/// #[derive(Serialize)]
324/// # #[serde(crate = "rocket::serde")]
325/// struct IndexContext<'a> {
326/// foo: u64,
327/// bar: &'a str,
328/// }
329///
330/// #[get("/<foo>")]
331/// fn render_index(foo: u64) -> Template {
332/// Template::render("index", IndexContext {
333/// foo,
334/// bar: "Hello world",
335/// })
336/// }
337/// ```
338///
339/// ## Nesting
340///
341/// Nested objects can be created by nesting calls to `context!`:
342///
343/// ```rust
344/// # use rocket_dyn_templates::context;
345/// # fn main() {
346/// let ctx = context! {
347/// planet: "Earth",
348/// info: context! {
349/// mass: 5.97e24,
350/// radius: "6371 km",
351/// moons: 1,
352/// },
353/// };
354/// # }
355/// ```
356#[macro_export]
357macro_rules! context {
358 ($($key:ident $(: $value:expr)?),*$(,)?) => {{
359 use $crate::serde::ser::{Serialize, Serializer, SerializeMap};
360 use ::std::fmt::{Debug, Formatter};
361 use ::std::result::Result;
362
363 #[allow(non_camel_case_types)]
364 struct ContextMacroCtxObject<$($key: Serialize),*> {
365 $($key: $key),*
366 }
367
368 #[allow(non_camel_case_types)]
369 impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> {
370 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
371 where S: Serializer,
372 {
373 let mut map = serializer.serialize_map(None)?;
374 $(map.serialize_entry(stringify!($key), &self.$key)?;)*
375 map.end()
376 }
377 }
378
379 #[allow(non_camel_case_types)]
380 impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> {
381 fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
382 f.debug_struct("context!")
383 $(.field(stringify!($key), &self.$key))*
384 .finish()
385 }
386 }
387
388 ContextMacroCtxObject {
389 $($key $(: $value)?),*
390 }
391 }};
392}