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::trace::Trace;
11use rocket::serde::Serialize;
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!()`](crate::context!)
144    /// macro, but it can be of any type that implements `Serialize`, such as
145    /// `HashMap` or a custom `struct`.
146    ///
147    /// To render a template directly into a string, use
148    /// [`Metadata::render()`](crate::Metadata::render()).
149    ///
150    /// # Examples
151    ///
152    /// Using the `context` macro:
153    ///
154    /// ```rust
155    /// use rocket_dyn_templates::{Template, context};
156    ///
157    /// let template = Template::render("index", context! {
158    ///     foo: "Hello, world!",
159    /// });
160    /// ```
161    ///
162    /// Using a `HashMap` as the context:
163    ///
164    /// ```rust
165    /// use std::collections::HashMap;
166    /// use rocket_dyn_templates::Template;
167    ///
168    /// // Create a `context` from a `HashMap`.
169    /// let mut context = HashMap::new();
170    /// context.insert("foo", "Hello, world!");
171    ///
172    /// let template = Template::render("index", context);
173    /// ```
174    #[inline]
175    pub fn render<S, C>(name: S, context: C) -> Template
176        where S: Into<Cow<'static, str>>, C: Serialize
177    {
178        Template {
179            name: name.into(),
180            value: Value::serialize(context),
181        }
182    }
183
184    /// Render the template named `name` with the context `context` into a
185    /// `String`. This method should **not** be used in any running Rocket
186    /// application. This method should only be used during testing to validate
187    /// `Template` responses. For other uses, use [`render()`](#method.render)
188    /// instead.
189    ///
190    /// The `context` can be of any type that implements `Serialize`. This is
191    /// typically a `HashMap` or a custom `struct`.
192    ///
193    /// Returns `Some` if the template could be rendered. Otherwise, returns
194    /// `None`. If rendering fails, error output is printed to the console.
195    /// `None` is also returned if a `Template` fairing has not been attached.
196    ///
197    /// # Example
198    ///
199    /// ```rust,no_run
200    /// # extern crate rocket;
201    /// # extern crate rocket_dyn_templates;
202    /// use std::collections::HashMap;
203    ///
204    /// use rocket_dyn_templates::Template;
205    /// use rocket::local::blocking::Client;
206    ///
207    /// fn main() {
208    ///     let rocket = rocket::build().attach(Template::fairing());
209    ///     let client = Client::untracked(rocket).expect("valid rocket");
210    ///
211    ///     // Create a `context`. Here, just an empty `HashMap`.
212    ///     let mut context = HashMap::new();
213    ///     # context.insert("test", "test");
214    ///     let template = Template::show(client.rocket(), "index", context);
215    /// }
216    /// ```
217    #[inline]
218    pub fn show<S, C>(rocket: &Rocket<Orbit>, name: S, context: C) -> Option<String>
219        where S: Into<Cow<'static, str>>, C: Serialize
220    {
221        let ctxt = rocket.state::<ContextManager>()
222            .map(ContextManager::context)
223            .or_else(|| {
224                error!("Uninitialized template context: missing fairing.\n\
225                    To use templates, you must attach `Template::fairing()`.\n\
226                    See the `Template` documentation for more information.");
227
228                None
229            })?;
230
231        Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1)
232    }
233
234    /// Actually render this template given a template context. This method is
235    /// called by the `Template` `Responder` implementation as well as
236    /// `Template::show()`.
237    #[inline(always)]
238    pub(crate) fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> {
239        let template = &*self.name;
240        let info = ctxt.templates.get(template).ok_or_else(|| {
241            let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
242            error!(
243                %template, search_path = %ctxt.root.display(), known_templates = ?ts,
244                "requested template not found"
245            );
246
247            Status::InternalServerError
248        })?;
249
250        let value = self.value.map_err(|e| {
251            span_error!("templating", "template context failed to serialize" => e.trace_error());
252            Status::InternalServerError
253        })?;
254
255        let string = ctxt.engines.render(template, info, value).ok_or_else(|| {
256            error!(template, "template failed to render");
257            Status::InternalServerError
258        })?;
259
260        Ok((info.data_type.clone(), string))
261    }
262}
263
264/// Returns a response with the Content-Type derived from the template's
265/// extension and a fixed-size body containing the rendered template. If
266/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
267impl<'r> Responder<'r, 'static> for Template {
268    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
269        let ctxt = req.rocket()
270            .state::<ContextManager>()
271            .ok_or_else(|| {
272                error!(
273                    "uninitialized template context: missing `Template::fairing()`.\n\
274                    To use templates, you must attach `Template::fairing()`."
275                );
276
277                Status::InternalServerError
278            })?;
279
280        self.finalize(&ctxt.context())?.respond_to(req)
281    }
282}
283
284impl Sentinel for Template {
285    fn abort(rocket: &Rocket<Ignite>) -> bool {
286        if rocket.state::<ContextManager>().is_none() {
287            error!(
288                "Missing `Template::fairing()`.\n\
289                 To use templates, you must attach `Template::fairing()`."
290            );
291
292            return true;
293        }
294
295        false
296    }
297}
298
299/// A macro to easily create a template rendering context.
300///
301/// Invocations of this macro expand to a value of an anonymous type which
302/// implements [`Serialize`]. Fields can be literal expressions or variables
303/// captured from a surrounding scope, as long as all fields implement
304/// `Serialize`.
305///
306/// # Examples
307///
308/// The following code:
309///
310/// ```rust
311/// # #[macro_use] extern crate rocket;
312/// # use rocket_dyn_templates::{Template, context};
313/// #[get("/<foo>")]
314/// fn render_index(foo: u64) -> Template {
315///     Template::render("index", context! {
316///         // Note that shorthand field syntax is supported.
317///         // This is equivalent to `foo: foo,`
318///         foo,
319///         bar: "Hello world",
320///     })
321/// }
322/// ```
323///
324/// is equivalent to the following, but without the need to manually define an
325/// `IndexContext` struct:
326///
327/// ```rust
328/// # use rocket_dyn_templates::Template;
329/// # use rocket::serde::Serialize;
330/// # use rocket::get;
331/// #[derive(Serialize)]
332/// # #[serde(crate = "rocket::serde")]
333/// struct IndexContext<'a> {
334///     foo: u64,
335///     bar: &'a str,
336/// }
337///
338/// #[get("/<foo>")]
339/// fn render_index(foo: u64) -> Template {
340///     Template::render("index", IndexContext {
341///         foo,
342///         bar: "Hello world",
343///     })
344/// }
345/// ```
346///
347/// ## Nesting
348///
349/// Nested objects can be created by nesting calls to `context!`:
350///
351/// ```rust
352/// # use rocket_dyn_templates::context;
353/// # fn main() {
354/// let ctx = context! {
355///     planet: "Earth",
356///     info: context! {
357///         mass: 5.97e24,
358///         radius: "6371 km",
359///         moons: 1,
360///     },
361/// };
362/// # }
363/// ```
364#[macro_export]
365macro_rules! context {
366    ($($key:ident $(: $value:expr)?),*$(,)?) => {{
367        use $crate::serde::ser::{Serialize, Serializer, SerializeMap};
368        use ::std::fmt::{Debug, Formatter};
369        use ::std::result::Result;
370
371        #[allow(non_camel_case_types)]
372        struct ContextMacroCtxObject<$($key: Serialize),*> {
373            $($key: $key),*
374        }
375
376        #[allow(non_camel_case_types)]
377        impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> {
378            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
379                where S: Serializer,
380            {
381                let mut map = serializer.serialize_map(None)?;
382                $(map.serialize_entry(stringify!($key), &self.$key)?;)*
383                map.end()
384            }
385        }
386
387        #[allow(non_camel_case_types)]
388        impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> {
389            fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
390                f.debug_struct("context!")
391                    $(.field(stringify!($key), &self.$key))*
392                    .finish()
393            }
394        }
395
396        ContextMacroCtxObject {
397            $($key $(: $value)?),*
398        }
399    }};
400}