Add templating support in contrib crate.

The contrib crate now contains support for both Handlebars and Tera. No
documentation yet.

resolves #5
This commit is contained in:
Sergio Benitez 2016-09-22 04:12:07 -07:00
parent 53e5377482
commit f74e286e31
17 changed files with 387 additions and 42 deletions

View File

@ -22,6 +22,7 @@ members = [
"examples/from_request", "examples/from_request",
"examples/stream", "examples/stream",
"examples/json", "examples/json",
"examples/handlebars_templates",
] ]
[replace] [replace]

View File

@ -6,11 +6,28 @@ authors = ["Sergio Benitez <sb@sergio.bz>"]
[features] [features]
default = ["json"] default = ["json"]
json = ["serde", "serde_json"] json = ["serde", "serde_json"]
tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"]
# Internal use only.
templates = ["serde", "serde_json", "lazy_static_macro", "glob"]
lazy_static_macro = ["lazy_static"]
[dependencies] [dependencies]
rocket = { path = "../lib/" } rocket = { path = "../lib/" }
log = "*" log = "*"
# JSON module dependencies # JSON and templating dependencies.
serde = { version = "*", optional = true } serde = { version = "*", optional = true }
serde_json = { version = "*", optional = true } serde_json = { version = "*", optional = true }
# Templating dependencies only.
handlebars = { version = "*", optional = true, features = ["serde_type"] }
glob = { version = "*", optional = true }
lazy_static = { version = "*", optional = true }
# Tera dependency
[dependencies.tera]
git = "https://github.com/SergioBenitez/tera"
branch = "array-get-filter"
optional = true

View File

@ -44,6 +44,7 @@ use self::serde_json::Error as JSONError;
/// } /// }
/// ``` /// ```
/// ///
#[derive(Debug)]
pub struct JSON<T>(pub T); pub struct JSON<T>(pub T);
impl<T> JSON<T> { impl<T> JSON<T> {

View File

@ -32,9 +32,19 @@
#[macro_use] extern crate log; #[macro_use] extern crate log;
#[macro_use] extern crate rocket; #[macro_use] extern crate rocket;
#[cfg_attr(feature = "lazy_static_macro", macro_use)]
#[cfg(feature = "lazy_static_macro")]
extern crate lazy_static;
#[cfg_attr(feature = "json", macro_use)] #[cfg_attr(feature = "json", macro_use)]
#[cfg(feature = "json")] #[cfg(feature = "json")]
mod json; mod json;
#[cfg(feature = "templates")]
mod templates;
#[cfg(feature = "json")] #[cfg(feature = "json")]
pub use json::JSON; pub use json::JSON;
#[cfg(feature = "templates")]
pub use templates::Template;

View File

@ -0,0 +1,35 @@
extern crate handlebars;
use std::sync::RwLock;
use super::serde::Serialize;
use super::TemplateInfo;
use self::handlebars::Handlebars;
lazy_static! {
static ref HANDLEBARS: RwLock<Handlebars> = RwLock::new(Handlebars::new());
}
pub const EXT: &'static str = "hbs";
pub fn render<T>(name: &str, info: &TemplateInfo, context: &T) -> Option<String>
where T: Serialize
{
// FIXME: Expose a callback to register each template at launch => no lock.
if HANDLEBARS.read().unwrap().get_template(name).is_none() {
let p = &info.full_path;
if let Err(e) = HANDLEBARS.write().unwrap().register_template_file(name, p) {
error_!("Handlebars template '{}' failed registry: {:?}", name, e);
return None;
}
}
match HANDLEBARS.read().unwrap().render(name, context) {
Ok(string) => Some(string),
Err(e) => {
error_!("Error rendering Handlebars template '{}': {}", name, e);
None
}
}
}

View File

@ -0,0 +1,44 @@
#[macro_export]
macro_rules! engine_set {
($($feature:expr => $engine:ident),+) => ({
use std::collections::HashSet;
let mut set = HashSet::new();
$(
#[cfg(feature = $feature)]
fn $engine(set: &mut HashSet<String>) {
set.insert($engine::EXT.to_string());
}
#[cfg(not(feature = $feature))]
fn $engine(_: &mut HashSet<String>) { }
$engine(&mut set);
)+
set
});
}
#[macro_export]
macro_rules! render_set {
($name:expr, $info:expr, $ctxt:expr, $($feature:expr => $engine:ident),+) => ({$(
#[cfg(feature = $feature)]
fn $engine<T: Serialize>(name: &str, info: &TemplateInfo, c: &T)
-> Option<Template> {
if info.extension == $engine::EXT {
let rendered = $engine::render(name, info, c);
return Some(Template(rendered, info.data_type.clone()));
}
None
}
#[cfg(not(feature = $feature))]
fn $engine<T: Serialize>(_: &str, _: &TemplateInfo, _: &T)
-> Option<Template> { None }
if let Some(template) = $engine($name, &$info, &$ctxt) {
return template
}
)+});
}

View File

@ -0,0 +1,102 @@
extern crate serde;
extern crate serde_json;
extern crate glob;
#[cfg(feature = "tera_templates")]
pub mod tera_templates;
#[cfg(feature = "handlebars_templates")]
pub mod handlebars_templates;
#[macro_use] mod macros;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use self::serde::Serialize;
use rocket::response::{data, Outcome, FreshHyperResponse, Responder};
use rocket::Rocket;
use self::glob::glob;
lazy_static! {
static ref TEMPLATES: HashMap<String, TemplateInfo> = discover_templates();
static ref TEMPLATE_DIR: String =
Rocket::config("template_dir").unwrap_or("templates").to_string();
}
/// Removes the file path's extension or does nothing if there is none.
fn remove_extension<P: AsRef<Path>>(path: P) -> PathBuf {
PathBuf::from(path.as_ref().file_stem().unwrap())
}
fn discover_templates() -> HashMap<String, TemplateInfo> {
// Keep this set in-sync with the `render_set` invocation.
let engines = engine_set![
"tera_templates" => tera_templates,
"handlebars_templates" => handlebars_templates
];
let mut templates = HashMap::new();
for ext in engines {
let mut path: PathBuf = [&*TEMPLATE_DIR, "**", "*"].iter().collect();
path.set_extension(ext);
for p in glob(path.to_str().unwrap()).unwrap().filter_map(Result::ok) {
let canonical_path = remove_extension(&p);
let name = remove_extension(&canonical_path);
let data_type = canonical_path.extension();
templates.insert(name.to_string_lossy().into_owned(), TemplateInfo {
full_path: p.to_path_buf(),
path: p.strip_prefix(&*TEMPLATE_DIR).unwrap().to_path_buf(),
canonical_path: canonical_path.clone(),
extension: p.extension().unwrap().to_string_lossy().into_owned(),
data_type: data_type.map(|d| d.to_string_lossy().into_owned())
});
}
}
templates
}
#[derive(Debug)]
pub struct Template(Option<String>, Option<String>);
#[derive(Debug)]
pub struct TemplateInfo {
full_path: PathBuf,
path: PathBuf,
canonical_path: PathBuf,
extension: String,
data_type: Option<String>
}
impl Template {
pub fn render<S, T>(name: S, context: T) -> Template
where S: AsRef<str>, T: Serialize
{
let name = name.as_ref();
let template = TEMPLATES.get(name);
if template.is_none() {
error_!("Template '{}' does not exist.", name);
return Template(None, None);
}
// Keep this set in-sync with the `engine_set` invocation.
render_set!(name, template.unwrap(), context,
"tera_templates" => tera_templates,
"handlebars_templates" => handlebars_templates
);
unreachable!("A template extension was discovered but not rendered.")
}
}
impl Responder for Template {
fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> {
match self.0 {
// FIXME: Detect the data type using the extension in self.1.
// Refactor response::named_file to use the extension map there.
Some(ref render) => data::HTML(render.as_str()).respond(res),
None => Outcome::Bad(res),
}
}
}

View File

@ -0,0 +1,39 @@
extern crate tera;
use std::path::PathBuf;
use self::tera::Renderer;
use super::serde::Serialize;
use super::serde_json;
use super::{TemplateInfo, TEMPLATE_DIR};
lazy_static! {
static ref TERA: tera::Tera = {
let path: PathBuf = [&*TEMPLATE_DIR, "**", "*.tera"].iter().collect();
tera::Tera::new(path.to_str().unwrap())
};
}
pub const EXT: &'static str = "tera";
pub fn render<T>(name: &str, info: &TemplateInfo, context: &T) -> Option<String>
where T: Serialize
{
let template = match TERA.get_template(&info.path.to_string_lossy()) {
Ok(template) => template,
Err(_) => {
error_!("Tera template '{}' does not exist.", name);
return None;
}
};
let value = serde_json::to_value(&context);
let mut renderer = Renderer::new_with_json(template, &TERA, value);
match renderer.render() {
Ok(string) => Some(string),
Err(e) => {
error_!("Error rendering Tera template '{}': {}", name, e);
None
}
}
}

View File

@ -0,0 +1,17 @@
[package]
name = "handlebars_templates"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
workspace = "../../"
[dependencies]
rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" }
serde = "*"
serde_macros = "*"
serde_json = "*"
[dependencies.rocket_contrib]
path = "../../contrib"
default-features = false
features = ["handlebars_templates"]

View File

@ -0,0 +1,44 @@
#![feature(plugin, custom_derive)]
#![plugin(rocket_codegen, serde_macros)]
extern crate rocket_contrib;
extern crate rocket;
extern crate serde_json;
use rocket::{Rocket, Request, Error};
use rocket::response::Redirect;
use rocket_contrib::Template;
#[derive(Serialize)]
struct TemplateContext {
name: String,
items: Vec<String>
}
#[get("/")]
fn index() -> Redirect {
Redirect::to("/hello/Unknown")
}
#[get("/hello/<name>")]
fn get(name: String) -> Template {
let context = TemplateContext {
name: name,
items: vec!["One", "Two", "Three"].iter().map(|s| s.to_string()).collect()
};
Template::render("index", context)
}
#[error(404)]
fn not_found<'r>(_: Error, req: &'r Request<'r>) -> Template {
let mut map = std::collections::HashMap::new();
map.insert("path", req.uri().as_str());
Template::render("404", map)
}
fn main() {
let mut rocket = Rocket::new("localhost", 8000);
rocket.catch(errors![not_found]);
rocket.mount_and_launch("/", routes![index, get]);
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>404</title>
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ path }} does not exist!
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Handlebars Demo</title>
</head>
<body>
<h1>Hi {{name}}</h1>
<h3>Here are your items:</h3>
<ul>
{{#each items}}
<Li>{{this}}</li>
{{/each}}
</ul>
</body>
</html>

View File

@ -8,9 +8,17 @@ workspace = "../../"
rocket = { path = "../../lib" } rocket = { path = "../../lib" }
rocket_codegen = { path = "../../codegen" } rocket_codegen = { path = "../../codegen" }
lazy_static = "*" lazy_static = "*"
tera = { git = "https://github.com/Keats/tera" }
serde = "0.8" serde = "0.8"
serde_json = "0.8" serde_json = "0.8"
serde_macros = "0.8" serde_macros = "0.8"
diesel = { git = "https://github.com/SergioBenitez/diesel", features = ["sqlite"] } diesel = { git = "https://github.com/SergioBenitez/diesel", features = ["sqlite"] }
diesel_codegen = { git = "https://github.com/SergioBenitez/diesel", default_features = false, features = ["sqlite"] } diesel_codegen = { git = "https://github.com/SergioBenitez/diesel", default_features = false, features = ["sqlite"] }
[dependencies.tera]
git = "https://github.com/SergioBenitez/tera"
branch = "array-get-filter"
[dependencies.rocket_contrib]
path = "../../contrib"
default_features = false
features = [ "tera_templates" ]

View File

@ -2,9 +2,9 @@
#![plugin(rocket_codegen, serde_macros, diesel_codegen)] #![plugin(rocket_codegen, serde_macros, diesel_codegen)]
extern crate rocket; extern crate rocket;
extern crate tera;
#[macro_use] extern crate diesel; #[macro_use] extern crate diesel;
#[macro_use] extern crate lazy_static; #[macro_use] extern crate lazy_static;
#[macro_use] extern crate rocket_contrib;
extern crate serde_json; extern crate serde_json;
mod static_files; mod static_files;
@ -12,63 +12,64 @@ mod task;
use rocket::Rocket; use rocket::Rocket;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket_contrib::Template;
use task::Task; use task::Task;
lazy_static!(static ref TERA: tera::Tera = tera::Tera::new("static/*.html");); #[derive(Debug, Serialize)]
struct Context<'a, 'b>{msg: Option<(&'a str, &'b str)>, tasks: Vec<Task>}
fn ctxt(msg: Option<(&str, &str)>) -> tera::Context { impl<'a, 'b> Context<'a, 'b> {
let unwrapped_msg = msg.unwrap_or(("", "")); pub fn err(msg: &'a str) -> Context<'static, 'a> {
let mut context = tera::Context::new(); Context{msg: Some(("error", msg)), tasks: Task::all()}
context.add("has_msg", &msg.is_some()); }
context.add("msg_type", &unwrapped_msg.0.to_string());
context.add("msg", &unwrapped_msg.1.to_string()); pub fn raw(msg: Option<(&'a str, &'b str)>) -> Context<'a, 'b> {
context.add("tasks", &Task::all()); Context{msg: msg, tasks: Task::all()}
context }
} }
#[post("/", form = "<todo>")] #[post("/", form = "<todo>")]
fn new(todo: Task) -> Result<Flash<Redirect>, tera::TeraResult<String>> { fn new(todo: Task) -> Result<Flash<Redirect>, Template> {
if todo.description.is_empty() { if todo.description.is_empty() {
let context = ctxt(Some(("error", "Description cannot be empty."))); Err(Template::render("index", Context::err("Description cannot be empty.")))
Err(TERA.render("index.html", context))
} else if todo.insert() { } else if todo.insert() {
Ok(Flash::success(Redirect::to("/"), "Todo successfully added.")) Ok(Flash::success(Redirect::to("/"), "Todo successfully added."))
} else { } else {
let context = ctxt(Some(("error", "Whoops! The server failed."))); Err(Template::render("index", Context::err("Whoops! The server failed.")))
Err(TERA.render("index.html", context))
} }
} }
// Should likely do something to simulate PUT. // Should likely do something to simulate PUT.
#[get("/<id>/toggle")] #[get("/<id>/toggle")]
fn toggle(id: i32) -> Result<Redirect, tera::TeraResult<String>> { fn toggle(id: i32) -> Result<Redirect, Template> {
if Task::toggle_with_id(id) { if Task::toggle_with_id(id) {
Ok(Redirect::to("/")) Ok(Redirect::to("/"))
} else { } else {
let context = ctxt(Some(("error", "Could not toggle that task."))); Err(Template::render("index", Context::err("Couldn't toggle task.")))
Err(TERA.render("index.html", context))
} }
} }
// Should likely do something to simulate DELETE. // Should likely do something to simulate DELETE.
#[get("/<id>/delete")] #[get("/<id>/delete")]
fn delete(id: i32) -> Result<Flash<Redirect>, tera::TeraResult<String>> { fn delete(id: i32) -> Result<Flash<Redirect>, Template> {
if Task::delete_with_id(id) { if Task::delete_with_id(id) {
Ok(Flash::success(Redirect::to("/"), "Todo was deleted.")) Ok(Flash::success(Redirect::to("/"), "Todo was deleted."))
} else { } else {
let context = ctxt(Some(("error", "Could not delete that task."))); Err(Template::render("index", Context::err("Couldn't delete task.")))
Err(TERA.render("index.html", context))
} }
} }
#[get("/")] #[get("/")]
fn index(msg: Option<Flash<()>>) -> tera::TeraResult<String> { fn index(msg: Option<Flash<()>>) -> Template {
TERA.render("index.html", ctxt(msg.as_ref().map(|m| (m.name(), m.msg())))) Template::render("index", match msg {
Some(ref msg) => Context::raw(Some((msg.name(), msg.msg()))),
None => Context::raw(None),
})
} }
fn main() { fn main() {
let mut rocket = Rocket::new("127.0.0.1", 8000); let mut rocket = Rocket::new("127.0.0.1", 8000);
rocket.mount("/", routes![index, static_files::all]) rocket.mount("/", routes![index, static_files::all])
.mount("/todo/", routes![new, delete, toggle]); .mount("/todo/", routes![new, toggle, delete]);
rocket.launch(); rocket.launch();
} }

View File

@ -23,10 +23,10 @@
<div class="ten columns"> <div class="ten columns">
<input type="text" placeholder="enter a task description..." <input type="text" placeholder="enter a task description..."
name="description" id="description" value="" autofocus name="description" id="description" value="" autofocus
class="u-full-width {% if has_msg %}field-{{msg_type}}{% endif %}" /> class="u-full-width {% if msg %}field-{{msg|first}}{% endif %}" />
{% if has_msg %} {% if msg %}
<small class="field-{{msg_type}}-msg"> <small class="field-{{msg|first}}-msg">
{{ msg }} {{ msg | get(i=1) }}
</small> </small>
{% endif %} {% endif %}
</div> </div>

View File

@ -3,6 +3,8 @@ use std::convert::AsRef;
use hyper::header::{SetCookie, CookiePair}; use hyper::header::{SetCookie, CookiePair};
use request::{Request, FromRequest}; use request::{Request, FromRequest};
const FLASH_COOKIE_NAME: &'static str = "_flash";
pub struct Flash<R> { pub struct Flash<R> {
name: String, name: String,
message: String, message: String,
@ -30,9 +32,9 @@ impl<R: Responder> Flash<R> {
Flash::new(responder, "error", msg) Flash::new(responder, "error", msg)
} }
pub fn cookie_pair(&self) -> CookiePair { fn cookie_pair(&self) -> CookiePair {
let content = format!("{}{}{}", self.name.len(), self.name, self.message); let content = format!("{}{}{}", self.name.len(), self.name, self.message);
let mut pair = CookiePair::new("_flash".to_string(), content); let mut pair = CookiePair::new(FLASH_COOKIE_NAME.to_string(), content);
pair.path = Some("/".to_string()); pair.path = Some("/".to_string());
pair.max_age = Some(300); pair.max_age = Some(300);
pair pair
@ -78,10 +80,10 @@ impl<'r, 'c> FromRequest<'r, 'c> for Flash<()> {
fn from_request(request: &'r Request<'c>) -> Result<Self, Self::Error> { fn from_request(request: &'r Request<'c>) -> Result<Self, Self::Error> {
trace_!("Flash: attemping to retrieve message."); trace_!("Flash: attemping to retrieve message.");
request.cookies().find("_flash").ok_or(()).and_then(|cookie| { request.cookies().find(FLASH_COOKIE_NAME).ok_or(()).and_then(|cookie| {
// Clear the flash message. // Clear the flash message.
trace_!("Flash: retrieving message: {:?}", cookie); trace_!("Flash: retrieving message: {:?}", cookie);
request.cookies().remove("flash"); request.cookies().remove(FLASH_COOKIE_NAME);
// Parse the flash. // Parse the flash.
let content = cookie.pair().1; let content = cookie.pair().1;

View File

@ -39,7 +39,8 @@ impl Rocket {
Ok(req) => req, Ok(req) => req,
Err(ref reason) => { Err(ref reason) => {
let mock_request = Request::mock(Method::Get, uri.as_str()); let mock_request = Request::mock(Method::Get, uri.as_str());
return self.handle_internal_error(reason, &mock_request, res); debug_!("Bad request: {}", reason);
return self.handle_internal_error(&mock_request, res);
} }
}; };
@ -65,10 +66,7 @@ impl Rocket {
res = match outcome { res = match outcome {
Outcome::Complete | Outcome::FailStop => return, Outcome::Complete | Outcome::FailStop => return,
Outcome::FailForward(r) => r, Outcome::FailForward(r) => r,
Outcome::Bad(r) => { Outcome::Bad(r) => return self.handle_internal_error(&request, r)
let reason = "Reason: bad response.";
return self.handle_internal_error(reason, &request, r)
}
}; };
} }
@ -77,10 +75,9 @@ impl Rocket {
} }
// Call on internal server error. // Call on internal server error.
fn handle_internal_error<'r>(&self, reason: &str, request: &'r Request<'r>, fn handle_internal_error<'r>(&self, request: &'r Request<'r>,
response: FreshHyperResponse) { response: FreshHyperResponse) {
error!("Internal server error."); error_!("Internal server error.");
debug!("{}", reason);
let catcher = self.catchers.get(&500).unwrap(); let catcher = self.catchers.get(&500).unwrap();
catcher.handle(Error::Internal, request).respond(response); catcher.handle(Error::Internal, request).respond(response);
} }