mirror of https://github.com/rwf2/Rocket.git
Add 'context!' for ad-hoc templating contexts.
This commit is contained in:
parent
6a3d1ac1d5
commit
8e3ad40beb
|
@ -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)?),*
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue