Add 'context!' for ad-hoc templating contexts.

This commit is contained in:
Flying-Toast 2021-05-13 09:17:33 -04:00 committed by Sergio Benitez
parent 6a3d1ac1d5
commit 8e3ad40beb
11 changed files with 284 additions and 72 deletions

View File

@ -119,7 +119,8 @@
//! Templates are rendered with the `render` method. The method takes in the
//! name of a template and a context to render the template with. The context
//! can be any type that implements [`Serialize`] and would serialize to an
//! `Object` value.
//! `Object` value. The [`context!`] macro can also be used to create inline
//! `Serialize`-able context objects.
//!
//! ## Automatic Reloading
//!
@ -167,6 +168,9 @@ 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;
@ -301,20 +305,32 @@ impl Template {
}
/// Render the template named `name` with the context `context`. The
/// `context` can be of any type that implements `Serialize`. This is
/// typically a `HashMap` or a custom `struct`.
/// `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`.
///
/// # Example
/// # 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`. Here, just an empty `HashMap`.
/// // Create a `context` from a `HashMap`.
/// let mut context = HashMap::new();
/// context.insert("foo", "Hello, world!");
///
/// # context.insert("test", "test");
/// # #[allow(unused_variables)]
/// let template = Template::render("index", context);
/// ```
#[inline]
@ -353,9 +369,7 @@ impl Template {
///
/// // Create a `context`. Here, just an empty `HashMap`.
/// let mut context = HashMap::new();
///
/// # context.insert("test", "test");
/// # #[allow(unused_variables)]
/// let template = Template::show(client.rocket(), "index", context);
/// }
/// ```
@ -435,3 +449,105 @@ impl Sentinel for Template {
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("/<foo>")]
/// fn render_index(foo: u64) -> Template {
/// Template::render("index", context! {
/// // Note that shorthand field syntax is supports.
/// // 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("/<foo>")]
/// 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};
#[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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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)?),*
}
}};
}

View File

@ -14,13 +14,12 @@ use crate::context::ContextManager;
/// ```rust
/// # #[macro_use] extern crate rocket;
/// # #[macro_use] extern crate rocket_dyn_templates;
/// use rocket_dyn_templates::{Template, Metadata};
/// use rocket_dyn_templates::{Template, Metadata, context};
///
/// #[get("/")]
/// fn homepage(metadata: Metadata) -> Template {
/// # use std::collections::HashMap;
/// # let context: HashMap<String, String> = HashMap::new();
/// // Conditionally render a template if it's available.
/// # let context = ();
/// if metadata.contains_template("some-template") {
/// Template::render("some-template", &context)
/// } else {

View File

@ -4,7 +4,9 @@ use std::path::{Path, PathBuf};
use rocket::{Rocket, Build};
use rocket::config::Config;
use rocket_dyn_templates::{Template, Metadata};
use rocket::figment::value::Value;
use rocket::serde::{Serialize, Deserialize};
use rocket_dyn_templates::{Template, Metadata, context};
#[get("/<engine>/<name>")]
fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> {
@ -98,6 +100,109 @@ fn test_sentinel() {
Client::debug_with(routes![always_ok_sentinel]).expect("no sentinel abort");
}
#[test]
fn test_context_macro() {
macro_rules! assert_same_object {
($ctx:expr, $obj:expr $(,)?) => {{
let ser_ctx = Value::serialize(&$ctx).unwrap();
let deser_ctx = ser_ctx.deserialize().unwrap();
assert_eq!($obj, deser_ctx);
}};
}
{
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Empty { }
assert_same_object!(context! { }, Empty { });
}
{
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Object {
a: u32,
b: String,
}
let a = 93;
let b = "Hello".to_string();
fn make_context() -> impl Serialize {
let b = "Hello".to_string();
context! { a: 93, b: b }
}
assert_same_object!(
make_context(),
Object { a, b },
);
}
{
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Outer {
s: String,
inner: Inner,
}
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Inner {
center: Center,
}
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Center {
value_a: bool,
value_b: u8,
}
let a = true;
let value_b = 123;
let outer_string = String::from("abc 123");
assert_same_object!(
context! {
s: &outer_string,
inner: context! {
center: context! {
value_a: a,
value_b,
},
},
},
Outer {
s: outer_string,
inner: Inner {
center: Center {
value_a: a,
value_b,
},
},
},
);
}
{
#[derive(Deserialize, PartialEq, Debug)]
#[serde(crate = "rocket::serde")]
struct Object {
a: String,
}
let owned = String::from("foo");
let ctx = context! { a: &owned };
assert_same_object!(ctx, Object { a: "foo".into() });
drop(ctx);
drop(owned);
}
}
#[cfg(feature = "tera")]
mod tera_tests {
use super::*;

View File

@ -1,9 +1,7 @@
use std::collections::HashMap;
use rocket::form::Form;
use rocket::response::Redirect;
use rocket::http::{Cookie, CookieJar};
use rocket_dyn_templates::Template;
use rocket_dyn_templates::{Template, context};
#[macro_export]
macro_rules! message_uri {
@ -21,12 +19,10 @@ fn submit(cookies: &CookieJar<'_>, message: Form<&str>) -> Redirect {
#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Template {
let cookie = cookies.get("message");
let mut context = HashMap::new();
if let Some(ref cookie) = cookie {
context.insert("message", cookie.value());
}
Template::render("message", &context)
Template::render("message", context! {
message: cookie.map(|c| c.value()),
})
}
pub fn routes() -> Vec<rocket::Route> {

View File

@ -1,12 +1,10 @@
use std::collections::HashMap;
use rocket::outcome::IntoOutcome;
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Redirect, Flash};
use rocket::http::{Cookie, CookieJar};
use rocket::form::Form;
use rocket_dyn_templates::Template;
use rocket_dyn_templates::{Template, context};
#[derive(FromForm)]
struct Login<'r> {
@ -39,9 +37,9 @@ pub use session_uri as uri;
#[get("/")]
fn index(user: User) -> Template {
let mut context = HashMap::new();
context.insert("user_id", user.0);
Template::render("session", &context)
Template::render("session", context! {
user_id: user.0,
})
}
#[get("/", rank = 2)]

View File

@ -1,21 +1,10 @@
use rocket::Request;
use rocket::response::Redirect;
use rocket::serde::Serialize;
use rocket_dyn_templates::{Template, handlebars};
use rocket_dyn_templates::{Template, handlebars, context};
use self::handlebars::{Handlebars, JsonRender};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct TemplateContext<'r> {
title: &'r str,
name: Option<&'r str>,
items: Vec<&'r str>,
// This special key tells handlebars which template is the parent.
parent: &'static str,
}
#[get("/")]
pub fn index() -> Redirect {
Redirect::to(uri!("/hbs", hello(name = "Your Name")))
@ -23,27 +12,28 @@ pub fn index() -> Redirect {
#[get("/hello/<name>")]
pub fn hello(name: &str) -> Template {
Template::render("hbs/index", &TemplateContext {
Template::render("hbs/index", context! {
title: "Hello",
name: Some(name),
items: vec!["One", "Two", "Three"],
// This special key tells handlebars which template is the parent.
parent: "hbs/layout",
})
}
#[get("/about")]
pub fn about() -> Template {
let mut map = std::collections::HashMap::new();
map.insert("title", "About");
map.insert("parent", "hbs/layout");
Template::render("hbs/about.html", &map)
Template::render("hbs/about.html", context! {
title: "About",
parent: "hbs/layout",
})
}
#[catch(404)]
pub fn not_found(req: &Request<'_>) -> Template {
let mut map = std::collections::HashMap::new();
map.insert("path", req.uri().path().raw());
Template::render("hbs/error/404", &map)
Template::render("hbs/error/404", context! {
uri: req.uri()
})
}
fn wow_helper(

View File

@ -1,18 +1,7 @@
use std::collections::HashMap;
use rocket::Request;
use rocket::response::Redirect;
use rocket::serde::Serialize;
use rocket_dyn_templates::{Template, tera::Tera};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct TemplateContext<'r> {
title: &'r str,
name: Option<&'r str>,
items: Vec<&'r str>
}
use rocket_dyn_templates::{Template, tera::Tera, context};
#[get("/")]
pub fn index() -> Redirect {
@ -21,7 +10,7 @@ pub fn index() -> Redirect {
#[get("/hello/<name>")]
pub fn hello(name: &str) -> Template {
Template::render("tera/index", &TemplateContext {
Template::render("tera/index", context! {
title: "Hello",
name: Some(name),
items: vec!["One", "Two", "Three"],
@ -30,16 +19,16 @@ pub fn hello(name: &str) -> Template {
#[get("/about")]
pub fn about() -> Template {
let mut map = HashMap::new();
map.insert("title", "About");
Template::render("tera/about.html", &map)
Template::render("tera/about.html", context! {
title: "About",
})
}
#[catch(404)]
pub fn not_found(req: &Request<'_>) -> Template {
let mut map = HashMap::new();
map.insert("path", req.uri().path().raw());
Template::render("tera/error/404", &map)
Template::render("tera/error/404", context! {
uri: req.uri()
})
}
pub fn customize(tera: &mut Tera) {

View File

@ -2,7 +2,7 @@ use super::rocket;
use rocket::http::{RawStr, Status, Method::*};
use rocket::local::blocking::Client;
use rocket_dyn_templates::Template;
use rocket_dyn_templates::{Template, context};
fn test_root(kind: &str) {
// Check that the redirect works.
@ -18,9 +18,8 @@ fn test_root(kind: &str) {
// Check that other request methods are not accepted (and instead caught).
for method in &[Post, Put, Delete, Options, Trace, Connect, Patch] {
let mut map = std::collections::HashMap::new();
map.insert("path", format!("/{}", kind));
let expected = Template::show(client.rocket(), format!("{}/error/404", kind), &map);
let context = context! { uri: format!("/{}", kind) };
let expected = Template::show(client.rocket(), format!("{}/error/404", kind), &context);
let response = client.req(*method, format!("/{}", kind)).dispatch();
assert_eq!(response.status(), Status::NotFound);

View File

@ -6,6 +6,6 @@
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ path }} does not exist!
The page at {{ uri }} does not exist!
</body>
</html>

View File

@ -6,6 +6,6 @@
</head>
<body>
<h1>404: Hey! There's nothing here.</h1>
The page at {{ path }} does not exist!
The page at {{ uri }} does not exist!
</body>
</html>

View File

@ -441,6 +441,24 @@ a template and a context to render the template with. The context can be any
type that implements `Serialize` and serializes into an `Object` value, such as
structs, `HashMaps`, and others.
You can also use [`context!`] to create ad-hoc templating contexts without
defining a new type:
```rust
# #[macro_use] extern crate rocket;
# #[macro_use] extern crate rocket_dyn_templates;
# fn main() {}
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
Template::render("index", context! {
foo: 123,
})
}
```
For a template to be renderable, it must first be registered. The `Template`
fairing automatically registers all discoverable templates when attached. The
[Fairings](../fairings) sections of the guide provides more information on
@ -472,6 +490,8 @@ used.
the name `"index"` in templates, i.e, `{% extends "index" %}` or `{% extends
"base" %}` for `base.html.tera`.
[`context`]: @api/rocket_dyn_templates/macro.context.html
### Live Reloading
When your application is compiled in `debug` mode (without the `--release` flag