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}