mirror of https://github.com/rwf2/Rocket.git
parent
83cead775f
commit
491b04cf5a
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -45,9 +48,8 @@ impl Context {
|
|||
}
|
||||
}
|
||||
|
||||
Engines::init(&templates).map(|engines| {
|
||||
Context { root, templates, engines }
|
||||
})
|
||||
Engines::init(&templates)
|
||||
.map(|engines| Context { root, templates, engines } )
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()`.");
|
||||
|
|
|
@ -4,7 +4,9 @@ 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;
|
||||
|
||||
|
@ -12,6 +14,7 @@ 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};
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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.");
|
||||
|
@ -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
|
||||
/// 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;
|
||||
|
@ -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)
|
||||
|
|
|
@ -116,5 +116,61 @@ mod templates_tests {
|
|||
let response = client.get("/tera/test").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
fn test_template_reload() {
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use rocket::local::Client;
|
||||
|
||||
const RELOAD_TEMPLATE: &str = "hbs/reload";
|
||||
const INITIAL_TEXT: &str = "initial";
|
||||
const NEW_TEXT: &str = "reload";
|
||||
|
||||
fn write_file(path: &Path, text: &str) {
|
||||
let mut file = File::create(path).expect("open file");
|
||||
file.write_all(text.as_bytes()).expect("write file");
|
||||
file.sync_all().expect("sync file");
|
||||
}
|
||||
|
||||
let reload_path = Path::join(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")),
|
||||
"tests/templates/hbs/reload.txt.hbs"
|
||||
);
|
||||
|
||||
// set up the template before initializing the Rocket instance so
|
||||
// that it will be picked up in the initial loading of templates.
|
||||
write_file(&reload_path, INITIAL_TEXT);
|
||||
|
||||
let client = Client::new(rocket()).unwrap();
|
||||
|
||||
// verify that the initial content is correct
|
||||
let initial_rendered = Template::show(client.rocket(), RELOAD_TEMPLATE, ());
|
||||
assert_eq!(initial_rendered, Some(INITIAL_TEXT.into()));
|
||||
|
||||
// write a change to the file
|
||||
write_file(&reload_path, NEW_TEXT);
|
||||
|
||||
for _ in 0..6 {
|
||||
// dispatch any request to trigger a template reload
|
||||
client.get("/").dispatch();
|
||||
|
||||
// if the new content is correct, we are done
|
||||
let new_rendered = Template::show(client.rocket(), RELOAD_TEMPLATE, ());
|
||||
if new_rendered == Some(NEW_TEXT.into()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, retry a few times, waiting 250ms in between
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
|
||||
panic!("failed to reload modified template in 1.5s");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
reload
|
|
@ -263,6 +263,11 @@ engine used to render a template depends on the template file's extension. For
|
|||
example, if a file ends with `.hbs`, Handlebars is used, while if a file ends
|
||||
with `.tera`, Tera is used.
|
||||
|
||||
When your application is compiled in `debug` mode (without the `--release` flag
|
||||
passed to `cargo`), templates are automatically reloaded when they are modified.
|
||||
This means that you don't need to rebuild your application to observe template
|
||||
changes: simply refresh! In release builds, reloading is disabled.
|
||||
|
||||
For templates to be properly registered, the template fairing must be attached
|
||||
to the instance of Rocket. The [Fairings](/guide/fairings) sections of the guide
|
||||
provides more information on fairings. To attach the template fairing, simply
|
||||
|
|
Loading…
Reference in New Issue