From 8e3ad40beb6a14e606d22065ae537eea2378b79b Mon Sep 17 00:00:00 2001 From: Flying-Toast <38232168+Flying-Toast@users.noreply.github.com> Date: Thu, 13 May 2021 09:17:33 -0400 Subject: [PATCH] Add 'context!' for ad-hoc templating contexts. --- contrib/dyn_templates/src/lib.rs | 134 ++++++++++++++++-- contrib/dyn_templates/src/metadata.rs | 5 +- contrib/dyn_templates/tests/templates.rs | 107 +++++++++++++- examples/cookies/src/message.rs | 12 +- examples/cookies/src/session.rs | 10 +- examples/templating/src/hbs.rs | 30 ++-- examples/templating/src/tera.rs | 27 ++-- examples/templating/src/tests.rs | 7 +- .../templates/hbs/error/404.html.hbs | 2 +- .../templates/tera/error/404.html.tera | 2 +- site/guide/5-responses.md | 20 +++ 11 files changed, 284 insertions(+), 72 deletions(-) diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index f27193f5..5b918913 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -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("/")] +/// 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("/")] +/// 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(&self, serializer: S) -> Result + 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)?),* + } + }}; +} diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index b119c129..019a7c25 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -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 = HashMap::new(); /// // Conditionally render a template if it's available. +/// # let context = (); /// if metadata.contains_template("some-template") { /// Template::render("some-template", &context) /// } else { diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index a1c86a6c..bf28d06e 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -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("//")] 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::*; diff --git a/examples/cookies/src/message.rs b/examples/cookies/src/message.rs index 23151d34..aa1fb0d3 100644 --- a/examples/cookies/src/message.rs +++ b/examples/cookies/src/message.rs @@ -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 { diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index 831a475b..04ebeffe 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -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)] diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs index 87865ac3..7d47bdb5 100644 --- a/examples/templating/src/hbs.rs +++ b/examples/templating/src/hbs.rs @@ -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/")] 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( diff --git a/examples/templating/src/tera.rs b/examples/templating/src/tera.rs index 25ea9439..8e5e0b83 100644 --- a/examples/templating/src/tera.rs +++ b/examples/templating/src/tera.rs @@ -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/")] 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) { diff --git a/examples/templating/src/tests.rs b/examples/templating/src/tests.rs index 65ca39cd..0653a7c9 100644 --- a/examples/templating/src/tests.rs +++ b/examples/templating/src/tests.rs @@ -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); diff --git a/examples/templating/templates/hbs/error/404.html.hbs b/examples/templating/templates/hbs/error/404.html.hbs index 81fbc477..b1921f42 100644 --- a/examples/templating/templates/hbs/error/404.html.hbs +++ b/examples/templating/templates/hbs/error/404.html.hbs @@ -6,6 +6,6 @@

404: Hey! There's nothing here.

- The page at {{ path }} does not exist! + The page at {{ uri }} does not exist! diff --git a/examples/templating/templates/tera/error/404.html.tera b/examples/templating/templates/tera/error/404.html.tera index 748f1754..afda653d 100644 --- a/examples/templating/templates/tera/error/404.html.tera +++ b/examples/templating/templates/tera/error/404.html.tera @@ -6,6 +6,6 @@

404: Hey! There's nothing here.

- The page at {{ path }} does not exist! + The page at {{ uri }} does not exist! diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 623d3a0a..2ee90249 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -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