Skip to content

Commit

Permalink
Implement template auto-reload.
Browse files Browse the repository at this point in the history
Resolves #163.
  • Loading branch information
jebrosen authored and SergioBenitez committed Aug 11, 2018
1 parent 83cead7 commit 491b04c
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 35 deletions.
3 changes: 3 additions & 0 deletions contrib/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,8 @@ tera = { version = "0.11", optional = true }
[dev-dependencies]
rocket_codegen = { version = "0.4.0-dev", path = "../../core/codegen" }

[target.'cfg(debug_assertions)'.dependencies]
notify = { version = "^4.0" }

[package.metadata.docs.rs]
all-features = true
12 changes: 7 additions & 5 deletions contrib/lib/src/templates/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ pub struct Context {
pub root: PathBuf,
/// Mapping from template name to its information.
pub templates: HashMap<String, TemplateInfo>,
/// Mapping from template name to its information.
pub engines: Engines
/// Loaded template engines
pub engines: Engines,
}

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: PathBuf) -> Option<Context> {
let mut templates: HashMap<String, TemplateInfo> = HashMap::new();
for ext in Engines::ENABLED_EXTENSIONS {
Expand Down Expand Up @@ -45,9 +48,8 @@ impl Context {
}
}

Engines::init(&templates).map(|engines| {
Context { root, templates, engines }
})
Engines::init(&templates)
.map(|engines| Context { root, templates, engines } )
}
}

Expand Down
168 changes: 168 additions & 0 deletions contrib/lib/src/templates/fairing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use super::DEFAULT_TEMPLATE_DIR;
use super::context::Context;
use super::engine::Engines;

use rocket::Rocket;
use rocket::config::ConfigError;
use rocket::fairing::{Fairing, Info, Kind};

pub use self::context::ContextManager;

#[cfg(not(debug_assertions))]
mod context {
use std::ops::Deref;
use super::Context;

/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub 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
}
}
}

#[cfg(debug_assertions)]
mod context {
extern crate notify;

use std::ops::{Deref, DerefMut};
use std::sync::{RwLock, Mutex};
use std::sync::mpsc::{channel, Receiver};

use super::{Context, Engines};

use self::notify::{raw_watcher, RawEvent, RecommendedWatcher, RecursiveMode, Watcher};

/// Wraps a Context. With `cfg(debug_assertions)` active, this structure
/// additionally provides a method to reload the context at runtime.
pub 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 = if let Ok(mut watcher) = raw_watcher(tx) {
if watcher.watch(ctxt.root.clone(), RecursiveMode::Recursive).is_ok() {
Some((watcher, Mutex::new(rx)))
} else {
warn!("Could not monitor the templates directory for changes.");
warn_!("Live template reload will be unavailable");
None
}
} else {
warn!("Could not instantiate a filesystem watcher.");
warn_!("Live template reload will be unavailable");
None
};

ContextManager {
watcher,
context: RwLock::new(ctxt),
}
}

pub fn context<'a>(&'a self) -> impl Deref<Target=Context> + 'a {
self.context.read().unwrap()
}

fn context_mut<'a>(&'a self) -> impl DerefMut<Target=Context> + 'a {
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<F: Fn(&mut Engines)>(&self, custom_callback: F) {
self.watcher.as_ref().map(|w| {
let rx = w.1.lock().expect("receive queue");
let mut changed = false;
while let Ok(_) = rx.try_recv() {
changed = true;
}

if changed {
info_!("Change detected: reloading templates.");
let mut ctxt = self.context_mut();
if let Some(mut new_ctxt) = Context::initialize(ctxt.root.clone()) {
custom_callback(&mut new_ctxt.engines);
*ctxt = new_ctxt;
} else {
warn_!("An error occurred while reloading templates.");
warn_!("The previous 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
/// if necessary.
pub struct TemplateFairing {
/// The user-provided customization callback, allowing the use of
/// functionality specific to individual template engines. In debug mode,
/// this callback might be run multiple times as templates are reloaded.
pub(crate) custom_callback: Box<Fn(&mut Engines) + Send + Sync + 'static>,
}

impl Fairing for TemplateFairing {
fn info(&self) -> Info {
// The on_request part of this fairing only applies in debug
// mode, so only register it in debug mode.
Info {
name: "Templates",
#[cfg(debug_assertions)]
kind: Kind::Attach | Kind::Request,
#[cfg(not(debug_assertions))]
kind: Kind::Attach,
}
}

/// Initializes the template context. Templates will be searched for in the
/// `template_dir` config variable or the default ([DEFAULT_TEMPLATE_DIR]).
/// The user's callback, if any was supplied, is called to customize the
/// template engines. In debug mode, the `ContextManager::new` method
/// initializes a directory watcher for auto-reloading of templates.
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
let mut template_root = rocket.config().root_relative(DEFAULT_TEMPLATE_DIR);
match rocket.config().get_str("template_dir") {
Ok(dir) => template_root = rocket.config().root_relative(dir),
Err(ConfigError::NotFound) => { /* ignore missing configs */ }
Err(e) => {
e.pretty_print();
warn_!("Using default templates directory '{:?}'", template_root);
}
};

match Context::initialize(template_root) {
Some(mut ctxt) => {
(self.custom_callback)(&mut ctxt.engines);
Ok(rocket.manage(ContextManager::new(ctxt)))
}
None => Err(rocket),
}
}

#[cfg(debug_assertions)]
fn on_request(&self, req: &mut ::rocket::Request, _data: &::rocket::Data) {
let cm = req.guard::<::rocket::State<ContextManager>>()
.expect("Template ContextManager registered in on_attach");

cm.reload_if_needed(&*self.custom_callback);
}
}
10 changes: 5 additions & 5 deletions contrib/lib/src/templates/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rocket::{Request, State, Outcome};
use rocket::http::Status;
use rocket::request::{self, FromRequest};

use templates::Context;
use super::ContextManager;

/// The `TemplateMetadata` type: implements `FromRequest`, allowing dynamic
/// queries about template metadata.
Expand Down Expand Up @@ -48,7 +48,7 @@ use templates::Context;
/// }
/// }
/// ```
pub struct TemplateMetadata<'a>(&'a Context);
pub struct TemplateMetadata<'a>(&'a ContextManager);

impl<'a> TemplateMetadata<'a> {
/// Returns `true` if the template with name `name` was loaded at start-up
Expand All @@ -65,7 +65,7 @@ impl<'a> TemplateMetadata<'a> {
/// }
/// ```
pub fn contains_template(&self, name: &str) -> bool {
self.0.templates.contains_key(name)
self.0.context().templates.contains_key(name)
}
}

Expand All @@ -76,9 +76,9 @@ impl<'a, 'r> FromRequest<'a, 'r> for TemplateMetadata<'a> {
type Error = ();

fn from_request(request: &'a Request) -> request::Outcome<Self, ()> {
request.guard::<State<Context>>()
request.guard::<State<ContextManager>>()
.succeeded()
.and_then(|ctxt| Some(Outcome::Success(TemplateMetadata(ctxt.inner()))))
.and_then(|cm| Some(Outcome::Success(TemplateMetadata(cm.inner()))))
.unwrap_or_else(|| {
error_!("Uninitialized template context: missing fairing.");
info_!("To use templates, you must attach `Template::fairing()`.");
Expand Down
44 changes: 19 additions & 25 deletions contrib/lib/src/templates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ extern crate glob;

#[cfg(feature = "tera_templates")] mod tera_templates;
#[cfg(feature = "handlebars_templates")] mod handlebars_templates;

mod engine;
mod fairing;
mod context;
mod metadata;

pub use self::engine::Engines;
pub use self::metadata::TemplateMetadata;

use self::engine::Engine;
use self::fairing::{TemplateFairing, ContextManager};
use self::context::Context;
use self::serde::Serialize;
use self::serde_json::{Value, to_value};
Expand All @@ -22,10 +25,9 @@ use std::path::PathBuf;

use rocket::{Rocket, State};
use rocket::request::Request;
use rocket::fairing::{Fairing, AdHoc};
use rocket::fairing::Fairing;
use rocket::response::{self, Content, Responder};
use rocket::http::{ContentType, Status};
use rocket::config::ConfigError;

const DEFAULT_TEMPLATE_DIR: &'static str = "templates";

Expand Down Expand Up @@ -76,6 +78,11 @@ const DEFAULT_TEMPLATE_DIR: &'static str = "templates";
/// [Serde](https://github.com/serde-rs/json) and would serialize to an `Object`
/// value.
///
/// 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.
///
/// # Usage
///
/// To use, add the `handlebars_templates` feature, the `tera_templates`
Expand Down Expand Up @@ -205,26 +212,10 @@ impl Template {
/// # ;
/// }
/// ```
pub fn custom<F>(f: F) -> impl Fairing where F: Fn(&mut Engines) + Send + Sync + 'static {
AdHoc::on_attach(move |rocket| {
let mut template_root = rocket.config().root_relative(DEFAULT_TEMPLATE_DIR);
match rocket.config().get_str("template_dir") {
Ok(dir) => template_root = rocket.config().root_relative(dir),
Err(ConfigError::NotFound) => { /* ignore missing configs */ }
Err(e) => {
e.pretty_print();
warn_!("Using default templates directory '{:?}'", template_root);
}
};

match Context::initialize(template_root) {
Some(mut ctxt) => {
f(&mut ctxt.engines);
Ok(rocket.manage(ctxt))
}
None => Err(rocket)
}
})
pub fn custom<F>(f: F) -> impl Fairing
where F: Fn(&mut Engines) + Send + Sync + 'static
{
TemplateFairing { custom_callback: Box::new(f) }
}

/// Render the template named `name` with the context `context`. The
Expand Down Expand Up @@ -289,7 +280,7 @@ impl Template {
pub fn show<S, C>(rocket: &Rocket, name: S, context: C) -> Option<String>
where S: Into<Cow<'static, str>>, C: Serialize
{
let ctxt = rocket.state::<Context>().or_else(|| {
let ctxt = rocket.state::<ContextManager>().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.");
Expand All @@ -299,6 +290,9 @@ impl Template {
Template::render(name, context).finalize(&ctxt).ok().map(|v| v.0)
}

/// Aactually render this template given a template context. This method is

This comment has been minimized.

Copy link
@Cxarli

Cxarli Aug 11, 2018

Typo: Aactually -> Actually

/// called by the `Template` `Responder` implementation as well as
/// `Template::show()`.
#[inline(always)]
fn finalize(self, ctxt: &Context) -> Result<(String, ContentType), Status> {
let name = &*self.name;
Expand Down Expand Up @@ -329,12 +323,12 @@ impl Template {
/// rendering fails, an `Err` of `Status::InternalServerError` is returned.
impl Responder<'static> for Template {
fn respond_to(self, req: &Request) -> response::Result<'static> {
let ctxt = req.guard::<State<Context>>().succeeded().ok_or_else(|| {
let ctxt = req.guard::<State<ContextManager>>().succeeded().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
})?;
})?.inner().context();

let (render, content_type) = self.finalize(&ctxt)?;
Content(content_type, render).respond_to(req)
Expand Down
Loading

1 comment on commit 491b04c

@Cxarli
Copy link

@Cxarli Cxarli commented on 491b04c Aug 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Please sign in to comment.