diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 2fffd09793..c62bc4fb15 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -17,28 +17,26 @@ type_complexity = "allow" multiple_bound_locations = "allow" [features] -tera = ["tera_"] -handlebars = ["handlebars_"] +tera = ["dep:tera"] +handlebars = ["dep:handlebars"] +minijinja = ["dep:minijinja"] [dependencies] walkdir = "2.4" notify = "6" normpath = "1" +tera = { version = "1.19.0", optional = true } +handlebars = { version = "5.1", optional = true } +minijinja = { version = "1.0.16", optional = true, features = ["loader"] } + [dependencies.rocket] version = "0.6.0-dev" path = "../../core/lib" default-features = false -[dependencies.tera_] -package = "tera" -version = "1.10.0" -optional = true - -[dependencies.handlebars_] -package = "handlebars" -version = "5.1" -optional = true +[dev-dependencies] +pretty_assertions = "1.4" [package.metadata.docs.rs] all-features = true diff --git a/contrib/dyn_templates/src/context.rs b/contrib/dyn_templates/src/context.rs index 344348ad30..33e260a0e0 100644 --- a/contrib/dyn_templates/src/context.rs +++ b/contrib/dyn_templates/src/context.rs @@ -2,7 +2,8 @@ use std::path::{Path, PathBuf}; use std::collections::HashMap; use std::error::Error; -use crate::{Engines, TemplateInfo}; +use crate::engine::Engines; +use crate::template::TemplateInfo; use rocket::http::ContentType; use normpath::PathExt; @@ -99,7 +100,7 @@ impl Context { #[cfg(not(debug_assertions))] mod manager { use std::ops::Deref; - use crate::Context; + use super::Context; /// Wraps a Context. With `cfg(debug_assertions)` active, this structure /// additionally provides a method to reload the context at runtime. diff --git a/contrib/dyn_templates/src/handlebars_templates.rs b/contrib/dyn_templates/src/engine/handlebars.rs similarity index 96% rename from contrib/dyn_templates/src/handlebars_templates.rs rename to contrib/dyn_templates/src/engine/handlebars.rs index 39b6dffa51..57f0c6ee56 100644 --- a/contrib/dyn_templates/src/handlebars_templates.rs +++ b/contrib/dyn_templates/src/engine/handlebars.rs @@ -1,9 +1,9 @@ use std::path::Path; +use handlebars::Handlebars; use rocket::serde::Serialize; use crate::engine::Engine; -pub use crate::handlebars::Handlebars; impl Engine for Handlebars<'static> { const EXT: &'static str = "hbs"; diff --git a/contrib/dyn_templates/src/engine/minijinja.rs b/contrib/dyn_templates/src/engine/minijinja.rs new file mode 100644 index 0000000000..88ae051f68 --- /dev/null +++ b/contrib/dyn_templates/src/engine/minijinja.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; +use std::path::Path; +use std::collections::HashMap; + +use rocket::serde::Serialize; +use minijinja::{Environment, Error, ErrorKind, AutoEscape}; + +use crate::engine::Engine; + +impl Engine for Environment<'static> { + const EXT: &'static str = "j2"; + + fn init<'a>(templates: impl Iterator) -> Option { + let _templates = Arc::new(templates + .map(|(k, p)| (k.to_owned(), p.to_owned())) + .collect::>()); + + let templates = _templates.clone(); + let mut env = Environment::new(); + env.set_loader(move |name| { + let Some(path) = templates.get(name) else { + return Ok(None); + }; + + match std::fs::read_to_string(path) { + Ok(result) => Ok(Some(result)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err( + Error::new(ErrorKind::InvalidOperation, "template read failed").with_source(e) + ), + } + }); + + let templates = _templates.clone(); + env.set_auto_escape_callback(move |name| { + templates.get(name) + .and_then(|path| path.to_str()) + .map(minijinja::default_auto_escape_callback) + .unwrap_or(AutoEscape::None) + }); + + Some(env) + } + + fn render(&self, name: &str, context: C) -> Option { + let Ok(template) = self.get_template(name) else { + error_!("Minijinja template '{name}' was not found."); + return None; + }; + + template.render(context) + .map_err(|e| error_!("Minijinja: {}", e)) + .ok() + } +} diff --git a/contrib/dyn_templates/src/engine.rs b/contrib/dyn_templates/src/engine/mod.rs similarity index 70% rename from contrib/dyn_templates/src/engine.rs rename to contrib/dyn_templates/src/engine/mod.rs index eba7230737..785b1c8425 100644 --- a/contrib/dyn_templates/src/engine.rs +++ b/contrib/dyn_templates/src/engine/mod.rs @@ -3,10 +3,22 @@ use std::collections::HashMap; use rocket::serde::Serialize; -use crate::TemplateInfo; +use crate::template::TemplateInfo; -#[cfg(feature = "tera")] use crate::tera::Tera; -#[cfg(feature = "handlebars")] use crate::handlebars::Handlebars; +#[cfg(feature = "tera")] +mod tera; +#[cfg(feature = "tera")] +use ::tera::Tera; + +#[cfg(feature = "handlebars")] +mod handlebars; +#[cfg(feature = "handlebars")] +use ::handlebars::Handlebars; + +#[cfg(feature = "minijinja")] +mod minijinja; +#[cfg(feature = "minijinja")] +use ::minijinja::Environment; pub(crate) trait Engine: Send + Sync + Sized + 'static { const EXT: &'static str; @@ -52,24 +64,38 @@ pub(crate) trait Engine: Send + Sync + Sized + 'static { /// [`tera::Value`]: crate::tera::Value /// [`tera::Result`]: crate::tera::Result pub struct Engines { - /// A `Tera` templating engine. This field is only available when the - /// `tera_templates` feature is enabled. When calling methods on the `Tera` - /// instance, ensure you use types imported from - /// `rocket_dyn_templates::tera` to avoid version mismatches. + /// A `Tera` templating engine. + /// + /// This field is only available when the `tera` feature is enabled. When + /// calling methods on the `Tera` instance, ensure you use types imported + /// from `rocket_dyn_templates::tera` to avoid version mismatches. #[cfg(feature = "tera")] pub tera: Tera, - /// The Handlebars templating engine. This field is only available when the - /// `handlebars_templates` feature is enabled. When calling methods on the - /// `Handlebars` instance, ensure you use types imported from - /// `rocket_dyn_templates::handlebars` to avoid version mismatches. + + /// The Handlebars templating engine. + /// + /// This field is only available when the `handlebars` feature is enabled. + /// When calling methods on the `Handlebars` instance, ensure you use types + /// imported from `rocket_dyn_templates::handlebars` to avoid version + /// mismatches. #[cfg(feature = "handlebars")] pub handlebars: Handlebars<'static>, + + /// The minijinja templating engine. + /// + /// This field is only available when the `minijinja` feature is enabled. + /// When calling methods on the [`Environment`] instance, ensure you use + /// types imported from `rocket_dyn_templates::minijinja` to avoid version + /// mismatches. + #[cfg(feature = "minijinja")] + pub minijinja: Environment<'static>, } impl Engines { pub(crate) const ENABLED_EXTENSIONS: &'static [&'static str] = &[ #[cfg(feature = "tera")] Tera::EXT, #[cfg(feature = "handlebars")] Handlebars::EXT, + #[cfg(feature = "minijinja")] Environment::EXT, ]; pub(crate) fn init(templates: &HashMap) -> Option { @@ -93,6 +119,11 @@ impl Engines { Some(hb) => hb, None => return None }, + #[cfg(feature = "minijinja")] + minijinja: match inner::>(templates) { + Some(hb) => hb, + None => return None + }, }) } @@ -100,7 +131,7 @@ impl Engines { &self, name: &str, info: &TemplateInfo, - context: C + context: C, ) -> Option { #[cfg(feature = "tera")] { if info.engine_ext == Tera::EXT { @@ -114,6 +145,12 @@ impl Engines { } } + #[cfg(feature = "minijinja")] { + if info.engine_ext == Environment::EXT { + return Engine::render(&self.minijinja, name, context); + } + } + None } diff --git a/contrib/dyn_templates/src/tera_templates.rs b/contrib/dyn_templates/src/engine/tera.rs similarity index 98% rename from contrib/dyn_templates/src/tera_templates.rs rename to contrib/dyn_templates/src/engine/tera.rs index 36d17cbfd5..7e783f7e0e 100644 --- a/contrib/dyn_templates/src/tera_templates.rs +++ b/contrib/dyn_templates/src/engine/tera.rs @@ -1,12 +1,11 @@ use std::path::Path; use std::error::Error; +use tera::{Context, Tera}; use rocket::serde::Serialize; use crate::engine::Engine; -pub use crate::tera::{Context, Tera}; - impl Engine for Tera { const EXT: &'static str = "tera"; diff --git a/contrib/dyn_templates/src/fairing.rs b/contrib/dyn_templates/src/fairing.rs index 8d10fec154..169bc2073b 100644 --- a/contrib/dyn_templates/src/fairing.rs +++ b/contrib/dyn_templates/src/fairing.rs @@ -1,9 +1,10 @@ -use crate::{DEFAULT_TEMPLATE_DIR, Context, Engines}; -use crate::context::{Callback, ContextManager}; - use rocket::{Rocket, Build, Orbit}; use rocket::fairing::{self, Fairing, Info, Kind}; +use crate::context::{Callback, Context, ContextManager}; +use crate::template::DEFAULT_TEMPLATE_DIR; +use crate::engine::Engines; + /// The TemplateFairing initializes the template system on attach, running /// custom_callback after templates have been loaded. In debug mode, the fairing /// checks for modifications to templates before every request and reloads them diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 8363f870fe..6ed0c36547 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -7,22 +7,34 @@ //! //! # Usage //! -//! 1. Enable the `rocket_dyn_templates` feature corresponding to your -//! templating engine(s) of choice: +//! 1. Depend on `rocket_dyn_templates`. Enable the feature(s) corresponding +//! to your templating engine(s) of choice: //! //! ```toml //! [dependencies.rocket_dyn_templates] //! version = "0.1.0" -//! features = ["handlebars", "tera"] +//! features = ["handlebars", "tera", "minijinja"] //! ``` //! -//! 1. Write your template files in Handlebars (`.hbs`) and/or Tera (`.tera`) -//! in the configurable `template_dir` directory (default: -//! `{rocket_root}/templates`). +//! 2. Write your templates inside of the [configurable] +//! `${ROCKET_ROOT}/templates`. The filename _must_ end with an extension +//! corresponding to an enabled engine. The second-to-last extension should +//! correspond to the file's type: //! -//! 2. Attach `Template::fairing()` return a `Template` using -//! `Template::render()`, supplying the name of the template file **minus -//! the last two extensions**: +//! | Engine | Extension | Example | +//! |--------------|-----------|--------------------------------------------| +//! | [Tera] | `.tera` | `${ROCKET_ROOT}/templates/index.html.tera` | +//! | [Handlebars] | `.hbs` | `${ROCKET_ROOT}/templates/index.html.hbs` | +//! | [MiniJinja] | `.j2` | `${ROCKET_ROOT}/templates/index.html.j2` | +//! +//! [configurable]: #configuration +//! [Tera]: https://docs.rs/crate/tera/1 +//! [Handlebars]: https://docs.rs/crate/handlebars/5 +//! [MiniJinja]: https://docs.rs/minijinja/1 +//! +//! 3. Attach `Template::fairing()` and return a [`Template`] from your routes +//! via [`Template::render()`], supplying the name of the template file +//! **minus the last two extensions**: //! //! ```rust //! # #[macro_use] extern crate rocket; @@ -30,7 +42,7 @@ //! //! #[get("/")] //! fn index() -> Template { -//! Template::render("template-name", context! { field: "value" }) +//! Template::render("index", context! { field: "value" }) //! } //! //! #[launch] @@ -39,98 +51,128 @@ //! } //! ``` //! -//! ## Naming +//! ## Configuration //! -//! 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. +//! This crate reads one configuration parameter from the configured figment: //! -//! 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. +//! * `template_dir` (**default: `templates/`**) //! -//! ## Content Type +//! A path to a directory to search for template files in. Relative paths +//! are considered relative to the configuration file, or there is no file, +//! the current working directory. //! -//! 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/master/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] | 5 | `.hbs` | -//! -//! [Tera]: https://docs.rs/crate/tera/1 -//! [Handlebars]: https://docs.rs/crate/handlebars/5 -//! -//! 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 | +//! For example, to change the default and set `template_dir` to different +//! values based on whether the application was compiled for debug or release +//! from a `Rocket.toml` file (read by the default figment), you might write: //! -//! 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. +//! ```toml +//! [debug] +//! template_dir = "static/templates" +//! +//! [release] +//! template_dir = "/var/opt/www/templates" +//! ``` +//! +//! **Note:** `template_dir` defaults to `templates/`. It _does not_ need to be +//! specified if the default suffices. //! -//! ## Template Fairing and Customization +//! See the [configuration chapter] of the guide for more information on +//! configuration. //! -//! 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. +//! [configuration chapter]: https://rocket.rs/master/guide/configuration //! -//! 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. +//! ## Template Naming and Content-Types //! -//! ## Rendering +//! Templates are rendered by _name_ via [`Template::render()`], which returns a +//! [`Template`] responder. The _name_ of the template is the path to the +//! template file, relative to `template_dir`, minus at most two extensions. //! -//! 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. +//! The `Content-Type` of the response is automatically determined by the +//! non-engine extension using [`ContentType::from_extension()`]. If there is no +//! such extension or it is unknown, `text/plain` is used. +//! +//! The following table contains examples: //! -//! 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. +//! | template path | [`Template::render()`] call | content-type | +//! |-----------------------------------------------|-----------------------------------|--------------| +//! | {template_dir}/index.html.hbs | `render("index")` | HTML | +//! | {template_dir}/index.tera | `render("index")` | `text/plain` | +//! | {template_dir}/index.hbs | `render("index")` | `text/plain` | +//! | {template_dir}/dir/index.hbs | `render("dir/index")` | `text/plain` | +//! | {template_dir}/dir/data.json.tera | `render("dir/data")` | JSON | +//! | {template_dir}/data.template.xml.hbs | `render("data.template")` | XML | +//! | {template_dir}/subdir/index.template.html.hbs | `render("subdir/index.template")` | HTML | //! -//! ## Automatic Reloading +//! 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`, and so +//! on. +//! +//! [`ContentType::from_extension()`]: ../rocket/http/struct.ContentType.html#method.from_extension +//! +//! ### Rendering Context +//! +//! In addition to a name, [`Template::render()`] requires a context to use +//! during rendering. The context can be any [`Serialize`] type that serializes +//! to an `Object` (a dictionary) value. The [`context!`] macro can be used to +//! create inline `Serialize`-able context objects. +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! use rocket::serde::Serialize; +//! use rocket_dyn_templates::{Template, context}; +//! +//! #[get("/")] +//! fn index() -> Template { +//! // Using the `context! { }` macro. +//! Template::render("index", context! { +//! site_name: "Rocket - Home Page", +//! version: 127, +//! }) +//! } +//! +//! #[get("/")] +//! fn index2() -> Template { +//! #[derive(Serialize)] +//! #[serde(crate = "rocket::serde")] +//! struct IndexContext { +//! site_name: &'static str, +//! version: u8 +//! } +//! +//! // Using an existing `IndexContext`, which implements `Serialize`. +//! Template::render("index", IndexContext { +//! site_name: "Rocket - Home Page", +//! version: 127, +//! }) +//! } +//! ``` +//! +//! ### Discovery, Automatic Reloads, and Engine Customization +//! +//! As long as one of [`Template::fairing()`], [`Template::custom()`], or +//! [`Template::try_custom()`] is [attached], any file in the configured +//! `template_dir` ending with a known engine extension (as described in the +//! [usage section](#usage)) can be rendered. The latter two fairings allow +//! customizations such as registering helpers and templates from strings. +//! +//! _**Note:** Templates that are registered directly via [`Template::custom()`], +//! use whatever name provided during that registration; no extensions are +//! automatically removed._ //! //! 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. +//! are **automatically reloaded** from disk when changes are made. In release +//! builds, template reloading is disabled to improve performance and cannot be +//! enabled. +//! +//! [attached]: Rocket::attach() +//! +//! ### Metadata and Rendering to `String` +//! +//! The [`Metadata`] request guard allows dynamically querying templating +//! metadata, such as whether a template is known to exist +//! ([`Metadata::contains_template()`]), and to render templates to `String` +//! ([`Metadata::render()`]). #![doc(html_root_url = "https://api.rocket.rs/master/rocket_dyn_templates")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] @@ -138,421 +180,30 @@ #[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 tera templating engine library, reexported. +pub use tera; -/// The handlebars templating engine library, reexported. #[doc(inline)] #[cfg(feature = "handlebars")] -pub use handlebars_ as handlebars; +/// The handlebars templating engine library, reexported. +pub use handlebars; -#[cfg(feature = "handlebars")] -mod handlebars_templates; +#[doc(inline)] +#[cfg(feature = "minijinja")] +/// The minijinja templating engine library, reexported. +pub use minijinja; + +#[doc(hidden)] +pub use rocket::serde; mod engine; mod fairing; mod context; mod metadata; +mod template; -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)?),* - } - }}; -} +pub use engine::Engines; +pub use metadata::Metadata; +pub use template::Template; diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index 9f52f33dfb..da774de1fd 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::borrow::Cow; use rocket::{Request, Rocket, Ignite, Sentinel}; @@ -124,6 +125,14 @@ impl Metadata<'_> { } } +impl fmt::Debug for Metadata<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map() + .entries(&self.0.context().templates) + .finish() + } +} + impl Sentinel for Metadata<'_> { fn abort(rocket: &Rocket) -> bool { if rocket.state::().is_none() { diff --git a/contrib/dyn_templates/src/template.rs b/contrib/dyn_templates/src/template.rs new file mode 100644 index 0000000000..3e275c05e5 --- /dev/null +++ b/contrib/dyn_templates/src/template.rs @@ -0,0 +1,392 @@ +use std::borrow::Cow; +use std::path::PathBuf; + +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; + +use crate::Engines; +use crate::fairing::TemplateFairing; +use crate::context::{Context, ContextManager}; + +pub(crate) 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. + pub(crate) path: Option, + /// The extension for the engine of this template. + pub(crate) engine_ext: &'static str, + /// The extension before the engine extension in the template, if any. + pub(crate) 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)] + pub(crate) 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)?),* + } + }}; +} diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index 8db5923df9..4f8d092f55 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -206,11 +206,12 @@ mod tera_tests { use std::collections::HashMap; use rocket::http::{ContentType, Status}; use rocket::request::FromRequest; + use pretty_assertions::assert_eq; const UNESCAPED_EXPECTED: &'static str - = "\nh_start\ntitle: _test_\nh_end\n\n\n