Discover manually registered templates.

This includes one breaking change: the default Content-Type of templates
without an identifying extension is now 'Text'. This is to prevent Tera
templates from rendering as HTML without being escaped.

Resolves #1637.
This commit is contained in:
Sergio Benitez 2021-05-18 11:38:14 -07:00
parent d03a07b183
commit 1c600bda30
18 changed files with 251 additions and 198 deletions

View File

@ -1,11 +1,15 @@
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::error::Error;
use crate::templates::{Engines, TemplateInfo};
use rocket::http::ContentType;
use normpath::PathExt;
pub(crate) type Callback =
Box<dyn Fn(&mut Engines) -> Result<(), Box<dyn Error>> + Send + Sync + 'static>;
pub(crate) struct Context {
/// The root of the template directory.
pub root: PathBuf,
@ -15,11 +19,13 @@ pub(crate) struct Context {
pub engines: Engines,
}
pub(crate) use self::manager::ContextManager;
impl Context {
/// Load all of the templates at `root`, initialize them using the relevant
/// template engine, and store all of the initialized state in a `Context`
/// structure, which is returned if all goes well.
pub fn initialize(root: &Path) -> Option<Context> {
pub fn initialize(root: &Path, callback: &Callback) -> Option<Context> {
let root = match root.normalize() {
Ok(root) => root.into_path_buf(),
Err(e) => {
@ -46,18 +52,134 @@ impl Context {
let data_type = data_type_str.as_ref()
.and_then(|ext| ContentType::from_extension(ext))
.unwrap_or(ContentType::HTML);
.unwrap_or(ContentType::Text);
templates.insert(name, TemplateInfo {
path: path.to_path_buf(),
extension: ext.to_string(),
path: Some(path.clone()),
engine_ext: ext,
data_type,
});
}
}
Engines::init(&templates)
.map(|engines| Context { root: root.into(), templates, engines } )
let mut engines = Engines::init(&templates)?;
if let Err(e) = callback(&mut engines) {
error_!("Template customization callback failed.");
error_!("{}", e);
return None;
}
for (name, engine_ext) in engines.templates() {
if !templates.contains_key(name) {
let data_type = Path::new(name).extension()
.and_then(|osstr| osstr.to_str())
.and_then(|ext| ContentType::from_extension(ext))
.unwrap_or(ContentType::Text);
let info = TemplateInfo { path: None, engine_ext, data_type };
templates.insert(name.to_string(), info);
}
}
Some(Context { root, templates, engines })
}
}
#[cfg(not(debug_assertions))]
mod manager {
use std::ops::Deref;
use crate::templates::Context;
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub(crate) struct ContextManager(Context);
impl ContextManager {
pub fn new(ctxt: Context) -> ContextManager {
ContextManager(ctxt)
}
pub fn context<'a>(&'a self) -> impl Deref<Target=Context> + 'a {
&self.0
}
pub fn is_reloading(&self) -> bool {
false
}
}
}
#[cfg(debug_assertions)]
mod manager {
use std::ops::{Deref, DerefMut};
use std::sync::{RwLock, Mutex};
use std::sync::mpsc::{channel, Receiver};
use notify::{raw_watcher, RawEvent, RecommendedWatcher, RecursiveMode, Watcher};
use super::{Callback, Context};
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub(crate) struct ContextManager {
/// The current template context, inside an RwLock so it can be updated.
context: RwLock<Context>,
/// A filesystem watcher and the receive queue for its events.
watcher: Option<(RecommendedWatcher, Mutex<Receiver<RawEvent>>)>,
}
impl ContextManager {
pub fn new(ctxt: Context) -> ContextManager {
let (tx, rx) = channel();
let watcher = raw_watcher(tx).and_then(|mut watcher| {
watcher.watch(ctxt.root.canonicalize()?, RecursiveMode::Recursive)?;
Ok(watcher)
});
let watcher = match watcher {
Ok(watcher) => Some((watcher, Mutex::new(rx))),
Err(e) => {
warn!("Failed to enable live template reloading: {}", e);
debug_!("Reload error: {:?}", e);
warn_!("Live template reloading is unavailable.");
None
}
};
ContextManager { watcher, context: RwLock::new(ctxt), }
}
pub fn context(&self) -> impl Deref<Target=Context> + '_ {
self.context.read().unwrap()
}
pub fn is_reloading(&self) -> bool {
self.watcher.is_some()
}
fn context_mut(&self) -> impl DerefMut<Target=Context> + '_ {
self.context.write().unwrap()
}
/// Checks whether any template files have changed on disk. If there
/// have been changes since the last reload, all templates are
/// reinitialized from disk and the user's customization callback is run
/// again.
pub fn reload_if_needed(&self, callback: &Callback) {
let templates_changes = self.watcher.as_ref()
.map(|(_, rx)| rx.lock().expect("fsevents lock").try_iter().count() > 0);
if let Some(true) = templates_changes {
info_!("Change detected: reloading templates.");
let root = self.context().root.clone();
if let Some(new_ctxt) = Context::initialize(&root, &callback) {
*self.context_mut() = new_ctxt;
} else {
warn_!("An error occurred while reloading templates.");
warn_!("Existing templates will remain active.");
};
}
}
}
}

View File

@ -1,3 +1,4 @@
use std::path::Path;
use std::collections::HashMap;
use serde::Serialize;
@ -7,10 +8,10 @@ use crate::templates::TemplateInfo;
#[cfg(feature = "tera_templates")] use crate::templates::tera::Tera;
#[cfg(feature = "handlebars_templates")] use crate::templates::handlebars::Handlebars;
pub(crate) trait Engine: Send + Sync + 'static {
pub(crate) trait Engine: Send + Sync + Sized + 'static {
const EXT: &'static str;
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Self> where Self: Sized;
fn init<'a>(templates: impl Iterator<Item = (&'a str, &'a Path)>) -> Option<Self>;
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String>;
}
@ -74,11 +75,11 @@ impl Engines {
pub(crate) fn init(templates: &HashMap<String, TemplateInfo>) -> Option<Engines> {
fn inner<E: Engine>(templates: &HashMap<String, TemplateInfo>) -> Option<E> {
let named_templates = templates.iter()
.filter(|&(_, i)| i.extension == E::EXT)
.map(|(k, i)| (k.as_str(), i))
.collect::<Vec<_>>();
.filter(|&(_, i)| i.engine_ext == E::EXT)
.filter_map(|(k, i)| Some((k.as_str(), i.path.as_ref()?)))
.map(|(k, p)| (k, p.as_path()));
E::init(&*named_templates)
E::init(named_templates)
}
Some(Engines {
@ -101,20 +102,37 @@ impl Engines {
info: &TemplateInfo,
context: C
) -> Option<String> {
#[cfg(feature = "tera_templates")]
{
if info.extension == Tera::EXT {
#[cfg(feature = "tera_templates")] {
if info.engine_ext == Tera::EXT {
return Engine::render(&self.tera, name, context);
}
}
#[cfg(feature = "handlebars_templates")]
{
if info.extension == Handlebars::EXT {
#[cfg(feature = "handlebars_templates")] {
if info.engine_ext == Handlebars::EXT {
return Engine::render(&self.handlebars, name, context);
}
}
None
}
pub(crate) fn templates(&self) -> impl Iterator<Item = (&str, &'static str)> {
#[cfg(all(feature = "tera_templates", feature = "handlebars_templates"))] {
self.tera.templates.keys()
.map(|name| (name.as_str(), Tera::EXT))
.chain(self.handlebars.get_templates().keys()
.map(|name| (name.as_str(), Handlebars::EXT)))
}
#[cfg(all(feature = "tera_templates", not(feature = "handlebars_templates")))] {
self.tera.templates.keys()
.map(|name| (name.as_str(), Tera::EXT))
}
#[cfg(all(feature = "handlebars_templates", not(feature = "tera_templates")))] {
self.handlebars.get_templates().keys()
.map(|name| (name.as_str(), Handlebars::EXT))
}
}
}

View File

@ -1,119 +1,9 @@
use std::error::Error;
use crate::templates::{DEFAULT_TEMPLATE_DIR, Context, Engines};
use crate::templates::context::{Callback, ContextManager};
use rocket::{Rocket, Build, Orbit};
use rocket::fairing::{self, Fairing, Info, Kind};
pub(crate) use self::context::ContextManager;
type Callback = Box<dyn Fn(&mut Engines) -> Result<(), Box<dyn Error>>+ Send + Sync + 'static>;
#[cfg(not(debug_assertions))]
mod context {
use std::ops::Deref;
use crate::templates::Context;
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub(crate) struct ContextManager(Context);
impl ContextManager {
pub fn new(ctxt: Context) -> ContextManager {
ContextManager(ctxt)
}
pub fn context<'a>(&'a self) -> impl Deref<Target=Context> + 'a {
&self.0
}
pub fn is_reloading(&self) -> bool {
false
}
}
}
#[cfg(debug_assertions)]
mod context {
use std::ops::{Deref, DerefMut};
use std::sync::{RwLock, Mutex};
use std::sync::mpsc::{channel, Receiver};
use notify::{raw_watcher, RawEvent, RecommendedWatcher, RecursiveMode, Watcher};
use super::{Callback, Context};
/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub(crate) struct ContextManager {
/// The current template context, inside an RwLock so it can be updated.
context: RwLock<Context>,
/// A filesystem watcher and the receive queue for its events.
watcher: Option<(RecommendedWatcher, Mutex<Receiver<RawEvent>>)>,
}
impl ContextManager {
pub fn new(ctxt: Context) -> ContextManager {
let (tx, rx) = channel();
let watcher = raw_watcher(tx).and_then(|mut watcher| {
watcher.watch(ctxt.root.canonicalize()?, RecursiveMode::Recursive)?;
Ok(watcher)
});
let watcher = match watcher {
Ok(watcher) => Some((watcher, Mutex::new(rx))),
Err(e) => {
warn!("Failed to enable live template reloading: {}", e);
debug_!("Reload error: {:?}", e);
warn_!("Live template reloading is unavailable.");
None
}
};
ContextManager { watcher, context: RwLock::new(ctxt), }
}
pub fn context(&self) -> impl Deref<Target=Context> + '_ {
self.context.read().unwrap()
}
pub fn is_reloading(&self) -> bool {
self.watcher.is_some()
}
fn context_mut(&self) -> impl DerefMut<Target=Context> + '_ {
self.context.write().unwrap()
}
/// Checks whether any template files have changed on disk. If there
/// have been changes since the last reload, all templates are
/// reinitialized from disk and the user's customization callback is run
/// again.
pub fn reload_if_needed(&self, callback: &Callback) {
let templates_changes = self.watcher.as_ref()
.map(|(_, rx)| rx.lock().expect("fsevents lock").try_iter().count() > 0);
if let Some(true) = templates_changes {
info_!("Change detected: reloading templates.");
let root = self.context().root.clone();
if let Some(mut new_ctxt) = Context::initialize(&root) {
match callback(&mut new_ctxt.engines) {
Ok(()) => *self.context_mut() = new_ctxt,
Err(e) => {
warn_!("The template customization callback returned an error:");
warn_!("{}", e);
warn_!("The existing templates will remain active.");
}
}
} else {
warn_!("An error occurred while reloading templates.");
warn_!("The existing templates will remain active.");
};
}
}
}
}
/// 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
@ -155,21 +45,11 @@ impl Fairing for TemplateFairing {
}
};
match Context::initialize(&path) {
Some(mut ctxt) => {
match (self.callback)(&mut ctxt.engines) {
Ok(()) => Ok(rocket.manage(ContextManager::new(ctxt))),
Err(e) => {
error_!("The template customization callback returned an error:");
error_!("{}", e);
Err(rocket)
}
}
}
None => {
error_!("Launch will be aborted due to failed template initialization.");
Err(rocket)
}
if let Some(ctxt) = Context::initialize(&path, &self.callback) {
Ok(rocket.manage(ContextManager::new(ctxt)))
} else {
error_!("Template initialization failed. Aborting launch.");
Err(rocket)
}
}

View File

@ -1,25 +1,26 @@
use serde::Serialize;
use std::path::Path;
use crate::templates::{Engine, TemplateInfo};
use serde::Serialize;
use crate::templates::Engine;
pub use crate::templates::handlebars::Handlebars;
impl Engine for Handlebars<'static> {
const EXT: &'static str = "hbs";
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Handlebars<'static>> {
fn init<'a>(templates: impl Iterator<Item = (&'a str, &'a Path)>) -> Option<Self> {
let mut hb = Handlebars::new();
for &(name, info) in templates {
let path = &info.path;
let mut ok = true;
for (name, path) in templates {
if let Err(e) = hb.register_template_file(name, path) {
error!("Error in Handlebars template '{}'.", name);
info_!("{}", e);
error!("Handlebars template '{}' failed to register.", name);
error_!("{}", e);
info_!("Template path: '{}'.", path.to_string_lossy());
return None;
ok = false;
}
}
Some(hb)
ok.then(|| hb)
}
fn render<C: Serialize>(&self, name: &str, context: C) -> Option<String> {

View File

@ -2,7 +2,7 @@ use rocket::{Request, Rocket, Ignite, Sentinel};
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use crate::templates::ContextManager;
use crate::templates::context::ContextManager;
/// Request guard for dynamically querying template metadata.
///

View File

@ -14,7 +14,7 @@
//! features = ["handlebars_templates", "tera_templates"]
//! ```
//!
//! 1. Write your template files in Handlebars (extension: `.hbs`) or tera
//! 1. Write your template files in Handlebars (extension: `.hbs`) and/or tera
//! (extensions: `.tera`) in the templates directory (default:
//! `{rocket_root}/templates`).
//!
@ -34,7 +34,7 @@
//! ```
//!
//! 3. Return a [`Template`] using [`Template::render()`], supplying the name
//! of the template file minus the last two extensions, from a handler.
//! of the template file **minus the last two extensions**, from a handler.
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
@ -49,6 +49,25 @@
//! }
//! ```
//!
//! ## 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.
//!
//! ## 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
@ -66,10 +85,10 @@
//! | Engine | Version | Extension |
//! |--------------|---------|-----------|
//! | [Tera] | 1 | `.tera` |
//! | [Handlebars] | 2 | `.hbs` |
//! | [Handlebars] | 3 | `.hbs` |
//!
//! [Tera]: https://docs.rs/crate/tera/1
//! [Handlebars]: https://docs.rs/crate/handlebars/2
//! [Handlebars]: https://docs.rs/crate/handlebars/3
//!
//! Any file that ends with one of these extension will be discovered and
//! rendered with the corresponding templating engine. The _name_ of the
@ -124,11 +143,10 @@ mod metadata;
pub use self::engine::Engines;
pub use self::metadata::Metadata;
pub(crate) use self::context::Context;
pub(crate) use self::fairing::ContextManager;
use self::engine::Engine;
use self::fairing::TemplateFairing;
use self::context::{Context, ContextManager};
use serde::Serialize;
use serde_json::{Value, to_value};
@ -205,10 +223,10 @@ pub struct Template {
#[derive(Debug)]
pub(crate) struct TemplateInfo {
/// The complete path, including `template_dir`, to this template.
path: PathBuf,
/// The complete path, including `template_dir`, to this template, if any.
path: Option<PathBuf>,
/// The extension for the engine of this template.
extension: String,
engine_ext: &'static str,
/// The extension before the engine extension in the template, if any.
data_type: ContentType
}

View File

@ -1,26 +1,26 @@
use serde::Serialize;
use std::path::Path;
use std::error::Error;
use crate::templates::{Engine, TemplateInfo};
use serde::Serialize;
use crate::templates::Engine;
pub use crate::templates::tera::{Context, Tera};
impl Engine for Tera {
const EXT: &'static str = "tera";
fn init(templates: &[(&str, &TemplateInfo)]) -> Option<Tera> {
fn init<'a>(templates: impl Iterator<Item = (&'a str, &'a Path)>) -> Option<Self> {
// Create the Tera instance.
let mut tera = Tera::default();
let ext = [".html.tera", ".htm.tera", ".xml.tera", ".html", ".htm", ".xml"];
tera.autoescape_on(ext.to_vec());
// Collect into a tuple of (name, path) for Tera.
let tera_templates = templates.iter()
.map(|&(name, info)| (&info.path, Some(name)))
.collect::<Vec<_>>();
// Collect into a tuple of (name, path) for Tera. If we register one at
// a time, it will complain about unregistered base templates.
let files = templates.map(|(name, path)| (path, Some(name)));
// Finally try to tell Tera about all of the templates.
if let Err(e) = tera.add_template_files(tera_templates) {
if let Err(e) = tera.add_template_files(files) {
error!("Failed to initialize Tera templating.");
let mut error = Some(&e as &dyn Error);

View File

@ -29,12 +29,10 @@ pub fn hello(name: &str) -> Template {
#[get("/about")]
pub fn about() -> Template {
Template::render("hbs/about", &TemplateContext {
title: "About",
name: None,
items: vec!["Some", "Important", "Info"],
parent: "hbs/layout",
})
let mut map = std::collections::HashMap::new();
map.insert("title", "About");
map.insert("parent", "hbs/layout");
Template::render("hbs/about.html", &map)
}
#[catch(404)]
@ -62,4 +60,14 @@ fn wow_helper(
pub fn customize(hbs: &mut Handlebars) {
hbs.register_helper("wow", Box::new(wow_helper));
hbs.register_template_string("hbs/about.html", r#"
{{#*inline "page"}}
<section id="about">
<h1>About - Here's another page!</h1>
</section>
{{/inline}}
{{~> (parent)~}}
"#).expect("valid HBS template");
}

View File

@ -17,7 +17,7 @@ fn index() -> Html<&'static str> {
fn rocket() -> _ {
rocket::build()
.mount("/", routes![index])
.mount("/tera", routes![tera::index, tera::hello])
.mount("/tera", routes![tera::index, tera::hello, tera::about])
.mount("/hbs", routes![hbs::index, hbs::hello, hbs::about])
.register("/hbs", catchers![hbs::not_found])
.register("/tera", catchers![tera::not_found])

View File

@ -7,7 +7,7 @@ use rocket_contrib::templates::{Template, tera::Tera};
#[derive(serde::Serialize)]
struct TemplateContext<'r> {
title: &'r str,
name: &'r str,
name: Option<&'r str>,
items: Vec<&'r str>
}
@ -19,12 +19,19 @@ pub fn index() -> Redirect {
#[get("/hello/<name>")]
pub fn hello(name: &str) -> Template {
Template::render("tera/index", &TemplateContext {
name,
title: "Hello",
name: Some(name),
items: vec!["One", "Two", "Three"],
})
}
#[get("/about")]
pub fn about() -> Template {
let mut map = HashMap::new();
map.insert("title", "About");
Template::render("tera/about.html", &map)
}
#[catch(404)]
pub fn not_found(req: &Request<'_>) -> Template {
let mut map = HashMap::new();
@ -32,6 +39,14 @@ pub fn not_found(req: &Request<'_>) -> Template {
Template::render("tera/error/404", &map)
}
pub fn customize(_tera: &mut Tera) {
/* register helpers, and so on */
pub fn customize(tera: &mut Tera) {
tera.add_raw_template("tera/about.html", r#"
{% extends "tera/base" %}
{% block content %}
<section id="about">
<h1>About - Here's another page!</h1>
</section>
{% endblock content %}
"#).expect("valid Tera template");
}

View File

@ -82,4 +82,5 @@ fn tera() {
test_root("tera");
test_name("tera");
test_404("tera");
test_about("tera");
}

View File

@ -1,13 +0,0 @@
{{#*inline "page"}}
<section id="about">
<h1>About - Here's another page!</h1>
<ul>
{{#each items}}
<li>{{ this }}</li>
{{/each}}
</ul>
</section>
{{/inline}}
{{~> (parent)~}}

View File

@ -5,9 +5,12 @@
<title>Tera Demo - {{ title }}</title>
</head>
<body>
<a href="/tera/hello/Unknown">Hello</a> | <a href="/tera/about">About</a>
{% block content %}{% endblock content %}
<footer>
<a href="/">Home</a>
</footer>
</body>
<footer>
<a href="/">Home</a>
</footer>
</html>