//! Dynamic templating engine support for Rocket. //! //! This crate adds support for dynamic template rendering to Rocket. It //! automatically discovers templates, provides a `Responder` to render //! templates, and automatically reloads templates when compiled in debug mode. //! At present, it supports [Handlebars] and [Tera]. //! //! # Usage //! //! 1. Enable the `rocket_dyn_templates` feature corresponding to your //! templating engine(s) of choice: //! //! ```toml //! [dependencies.rocket_dyn_templates] //! version = "=0.1.0-rc.3" //! features = ["handlebars", "tera"] //! ``` //! //! 1. Write your template files in Handlebars (`.hbs`) and/or Tera (`.tera`) //! in the configurable `template_dir` directory (default: //! `{rocket_root}/templates`). //! //! 2. Attach `Template::fairing()` return a `Template` using //! `Template::render()`, supplying the name of the template file **minus //! the last two extensions**: //! //! ```rust //! # #[macro_use] extern crate rocket; //! use rocket_dyn_templates::{Template, context}; //! //! #[get("/")] //! fn index() -> Template { //! Template::render("template-name", context! { field: "value" }) //! } //! //! #[launch] //! fn rocket() -> _ { //! rocket::build().attach(Template::fairing()) //! } //! ``` //! //! ## Naming //! //! Templates discovered by Rocket are _renamed_ from their file name to their //! file name **without the last two extensions**. As such, refer to a template //! with file name `foo.html.hbs` or `foo.html.tera` as `foo`. See //! [Discovery](#discovery) for more. //! //! Templates that are _not_ discovered by Rocket, such as those registered //! directly via [`Template::custom()`], are _not_ renamed. Use the name with //! which the template was originally registered. //! //! ## Content Type //! //! The `Content-Type` of the response is automatically determined by the //! non-engine extension of the template name or `text/plain` if there is no //! extension or the extension is unknown. For example, for a discovered //! template with file name `foo.html.hbs` or a manually registered template //! with name ending in `foo.html`, the `Content-Type` is automatically set to //! `ContentType::HTML`. //! //! ## Discovery //! //! Template names passed in to [`Template::render()`] must correspond to a //! previously discovered template in the configured template directory. The //! template directory is configured via the `template_dir` configuration //! parameter and defaults to `templates/`. The path set in `template_dir` is //! relative to the Rocket configuration file. See the [configuration //! chapter](https://rocket.rs/v0.5-rc/guide/configuration) of the guide for more //! information on configuration. //! //! The corresponding templating engine used for a given template is based on a //! template's extension. At present, this library supports the following //! engines and extensions: //! //! | Engine | Version | Extension | //! |--------------|---------|-----------| //! | [Tera] | 1 | `.tera` | //! | [Handlebars] | 4 | `.hbs` | //! //! [Tera]: https://docs.rs/crate/tera/1 //! [Handlebars]: https://docs.rs/crate/handlebars/4 //! //! Any file that ends with one of these extension will be discovered and //! rendered with the corresponding templating engine. The _name_ of the //! template will be the path to the template file relative to `template_dir` //! minus at most two extensions. The following table contains examples of this //! mapping: //! //! | example template path | template name | //! |-----------------------------------------------|-----------------------| //! | {template_dir}/index.html.hbs | index | //! | {template_dir}/index.tera | index | //! | {template_dir}/index.hbs | index | //! | {template_dir}/dir/index.hbs | dir/index | //! | {template_dir}/dir/index.html.tera | dir/index | //! | {template_dir}/index.template.html.hbs | index.template | //! | {template_dir}/subdir/index.template.html.hbs | subdir/index.template | //! //! The recommended naming scheme is to use two extensions: one for the file //! type, and one for the template extension. This means that template //! extensions should look like: `.html.hbs`, `.html.tera`, `.xml.hbs`, etc. //! //! ## Template Fairing and Customization //! //! Template discovery is actualized by the template fairing, which itself is //! created via [`Template::fairing()`], [`Template::custom()`], or //! [`Template::try_custom()`], the latter two allowing customizations to //! templating engines such as registering template helpers and register //! templates from strings. //! //! In order for _any_ templates to be rendered, the template fairing _must_ be //! [attached](rocket::Rocket::attach()) to the running Rocket instance. Failure //! to do so will result in an ignite-time error. //! //! ## Rendering //! //! Templates are typically rendered indirectly via [`Template::render()`] which //! returns a `Template` responder which renders the template at response time. //! To render a template directly into a `String`, use [`Metadata::render()`] //! instead. //! //! Both methods take in a template name and context to use while rendering. The //! context can be any [`Serialize`] type that serializes to an `Object` (a //! dictionary) value. The [`context!`] macro may be used to create inline //! `Serialize`-able context objects. //! //! ## Automatic Reloading //! //! In debug mode (without the `--release` flag passed to `cargo`), templates //! will be automatically reloaded from disk if any changes have been made to //! the templates directory since the previous request. In release builds, //! template reloading is disabled to improve performance and cannot be enabled. #![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_dyn_templates")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] #![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] #[macro_use] extern crate rocket; #[cfg(not(any(feature = "tera", feature = "handlebars")))] compile_error!("at least one of \"tera\" or \"handlebars\" features must be enabled"); /// The tera templating engine library, reexported. #[doc(inline)] #[cfg(feature = "tera")] pub use tera_ as tera; #[cfg(feature = "tera")] mod tera_templates; /// The handlebars templating engine library, reexported. #[doc(inline)] #[cfg(feature = "handlebars")] pub use handlebars_ as handlebars; #[cfg(feature = "handlebars")] mod handlebars_templates; mod engine; mod fairing; mod context; mod metadata; pub use self::engine::Engines; pub use self::metadata::Metadata; use self::fairing::TemplateFairing; use self::context::{Context, ContextManager}; use std::borrow::Cow; use std::path::PathBuf; #[doc(hidden)] pub use rocket::serde; use rocket::{Rocket, Orbit, Ignite, Sentinel}; use rocket::request::Request; use rocket::fairing::Fairing; use rocket::response::{self, Responder}; use rocket::http::{ContentType, Status}; use rocket::figment::{value::Value, error::Error}; use rocket::serde::Serialize; use rocket::yansi::Paint; const DEFAULT_TEMPLATE_DIR: &str = "templates"; /// Responder that renders a dynamic template. /// /// `Template` serves as a _proxy_ type for rendering a template and _does not_ /// contain the rendered template itself. The template is lazily rendered, at /// response time. To render a template greedily, use [`Template::show()`]. /// /// See the [crate root](crate) for usage details. #[derive(Debug)] pub struct Template { name: Cow<'static, str>, value: Result } #[derive(Debug)] pub(crate) struct TemplateInfo { /// The complete path, including `template_dir`, to this template, if any. path: Option, /// The extension for the engine of this template. engine_ext: &'static str, /// The extension before the engine extension in the template, if any. data_type: ContentType } impl Template { /// Returns a fairing that initializes and maintains templating state. /// /// This fairing, or the one returned by [`Template::custom()`], _must_ be /// attached to any `Rocket` instance that wishes to render templates. /// Failure to attach this fairing will result in a "Uninitialized template /// context: missing fairing." error message when a template is attempted to /// be rendered. /// /// If you wish to customize the internal templating engines, use /// [`Template::custom()`] instead. /// /// # Example /// /// To attach this fairing, simple call `attach` on the application's /// `Rocket` instance with `Template::fairing()`: /// /// ```rust /// extern crate rocket; /// extern crate rocket_dyn_templates; /// /// use rocket_dyn_templates::Template; /// /// fn main() { /// rocket::build() /// // ... /// .attach(Template::fairing()) /// // ... /// # ; /// } /// ``` pub fn fairing() -> impl Fairing { Template::custom(|_| {}) } /// Returns a fairing that initializes and maintains templating state. /// /// Unlike [`Template::fairing()`], this method allows you to configure /// templating engines via the function `f`. Note that only the enabled /// templating engines will be accessible from the `Engines` type. /// /// This method does not allow the function `f` to fail. If `f` is fallible, /// use [`Template::try_custom()`] instead. /// /// # Example /// /// ```rust /// extern crate rocket; /// extern crate rocket_dyn_templates; /// /// use rocket_dyn_templates::Template; /// /// fn main() { /// rocket::build() /// // ... /// .attach(Template::custom(|engines| { /// // engines.handlebars.register_helper ... /// })) /// // ... /// # ; /// } /// ``` pub fn custom(f: F) -> impl Fairing where F: Fn(&mut Engines) { Self::try_custom(move |engines| { f(engines); Ok(()) }) } /// Returns a fairing that initializes and maintains templating state. /// /// This variant of [`Template::custom()`] allows a fallible `f`. If `f` /// returns an error during initialization, it will cancel the launch. If /// `f` returns an error during template reloading (in debug mode), then the /// newly-reloaded templates are discarded. /// /// # Example /// /// ```rust /// extern crate rocket; /// extern crate rocket_dyn_templates; /// /// use rocket_dyn_templates::Template; /// /// fn main() { /// rocket::build() /// // ... /// .attach(Template::try_custom(|engines| { /// // engines.handlebars.register_helper ... /// Ok(()) /// })) /// // ... /// # ; /// } /// ``` pub fn try_custom(f: F) -> impl Fairing where F: Fn(&mut Engines) -> Result<(), Box> { TemplateFairing { callback: Box::new(f) } } /// Render the template named `name` with the context `context`. The /// `context` is typically created using the [`context!`] macro, but it can /// be of any type that implements `Serialize`, such as `HashMap` or a /// custom `struct`. /// /// To render a template directly into a string, use [`Metadata::render()`]. /// /// # Examples /// /// Using the `context` macro: /// /// ```rust /// use rocket_dyn_templates::{Template, context}; /// /// let template = Template::render("index", context! { /// foo: "Hello, world!", /// }); /// ``` /// /// Using a `HashMap` as the context: /// /// ```rust /// use std::collections::HashMap; /// use rocket_dyn_templates::Template; /// /// // Create a `context` from a `HashMap`. /// let mut context = HashMap::new(); /// context.insert("foo", "Hello, world!"); /// /// let template = Template::render("index", context); /// ``` #[inline] pub fn render(name: S, context: C) -> Template where S: Into>, C: Serialize { Template { name: name.into(), value: Value::serialize(context) } } /// Render the template named `name` with the context `context` into a /// `String`. This method should **not** be used in any running Rocket /// application. This method should only be used during testing to validate /// `Template` responses. For other uses, use [`render()`](#method.render) /// instead. /// /// The `context` can be of any type that implements `Serialize`. This is /// typically a `HashMap` or a custom `struct`. /// /// Returns `Some` if the template could be rendered. Otherwise, returns /// `None`. If rendering fails, error output is printed to the console. /// `None` is also returned if a `Template` fairing has not been attached. /// /// # Example /// /// ```rust,no_run /// # extern crate rocket; /// # extern crate rocket_dyn_templates; /// use std::collections::HashMap; /// /// use rocket_dyn_templates::Template; /// use rocket::local::blocking::Client; /// /// fn main() { /// let rocket = rocket::build().attach(Template::fairing()); /// let client = Client::untracked(rocket).expect("valid rocket"); /// /// // Create a `context`. Here, just an empty `HashMap`. /// let mut context = HashMap::new(); /// # context.insert("test", "test"); /// let template = Template::show(client.rocket(), "index", context); /// } /// ``` #[inline] pub fn show(rocket: &Rocket, name: S, context: C) -> Option where S: Into>, C: Serialize { let ctxt = rocket.state::().map(ContextManager::context).or_else(|| { warn!("Uninitialized template context: missing fairing."); info!("To use templates, you must attach `Template::fairing()`."); info!("See the `Template` documentation for more information."); None })?; Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1) } /// Actually render this template given a template context. This method is /// called by the `Template` `Responder` implementation as well as /// `Template::show()`. #[inline(always)] fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> { let name = &*self.name; let info = ctxt.templates.get(name).ok_or_else(|| { let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect(); error_!("Template '{}' does not exist.", name); info_!("Known templates: {}.", ts.join(", ")); info_!("Searched in {:?}.", ctxt.root); Status::InternalServerError })?; let value = self.value.map_err(|e| { error_!("Template context failed to serialize: {}.", e); Status::InternalServerError })?; let string = ctxt.engines.render(name, &info, value).ok_or_else(|| { error_!("Template '{}' failed to render.", name); Status::InternalServerError })?; Ok((info.data_type.clone(), string)) } } /// Returns a response with the Content-Type derived from the template's /// extension and a fixed-size body containing the rendered template. If /// rendering fails, an `Err` of `Status::InternalServerError` is returned. impl<'r> Responder<'r, 'static> for Template { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { let ctxt = req.rocket() .state::() .ok_or_else(|| { error_!("Uninitialized template context: missing fairing."); info_!("To use templates, you must attach `Template::fairing()`."); info_!("See the `Template` documentation for more information."); Status::InternalServerError })?; self.finalize(&ctxt.context())?.respond_to(req) } } impl Sentinel for Template { fn abort(rocket: &Rocket) -> bool { if rocket.state::().is_none() { let template = "Template".primary().bold(); let fairing = "Template::fairing()".primary().bold(); error!("returning `{}` responder without attaching `{}`.", template, fairing); info_!("To use or query templates, you must attach `{}`.", fairing); info_!("See the `Template` documentation for more information."); return true; } false } } /// A macro to easily create a template rendering context. /// /// Invocations of this macro expand to a value of an anonymous type which /// implements [`serde::Serialize`]. Fields can be literal expressions or /// variables captured from a surrounding scope, as long as all fields implement /// `Serialize`. /// /// # Examples /// /// The following code: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # use rocket_dyn_templates::{Template, context}; /// #[get("/")] /// fn render_index(foo: u64) -> Template { /// Template::render("index", context! { /// // Note that shorthand field syntax is supported. /// // This is equivalent to `foo: foo,` /// foo, /// bar: "Hello world", /// }) /// } /// ``` /// /// is equivalent to the following, but without the need to manually define an /// `IndexContext` struct: /// /// ```rust /// # use rocket_dyn_templates::Template; /// # use rocket::serde::Serialize; /// # use rocket::get; /// #[derive(Serialize)] /// # #[serde(crate = "rocket::serde")] /// struct IndexContext<'a> { /// foo: u64, /// bar: &'a str, /// } /// /// #[get("/")] /// fn render_index(foo: u64) -> Template { /// Template::render("index", IndexContext { /// foo, /// bar: "Hello world", /// }) /// } /// ``` /// /// ## Nesting /// /// Nested objects can be created by nesting calls to `context!`: /// /// ```rust /// # use rocket_dyn_templates::context; /// # fn main() { /// let ctx = context! { /// planet: "Earth", /// info: context! { /// mass: 5.97e24, /// radius: "6371 km", /// moons: 1, /// }, /// }; /// # } /// ``` #[macro_export] macro_rules! context { ($($key:ident $(: $value:expr)?),*$(,)?) => {{ use $crate::serde::ser::{Serialize, Serializer, SerializeMap}; use ::std::fmt::{Debug, Formatter}; use ::std::result::Result; #[allow(non_camel_case_types)] struct ContextMacroCtxObject<$($key: Serialize),*> { $($key: $key),* } #[allow(non_camel_case_types)] impl<$($key: Serialize),*> Serialize for ContextMacroCtxObject<$($key),*> { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut map = serializer.serialize_map(None)?; $(map.serialize_entry(stringify!($key), &self.$key)?;)* map.end() } } #[allow(non_camel_case_types)] impl<$($key: Debug + Serialize),*> Debug for ContextMacroCtxObject<$($key),*> { fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result { f.debug_struct("context!") $(.field(stringify!($key), &self.$key))* .finish() } } ContextMacroCtxObject { $($key $(: $value)?),* } }}; }