From 9b3c83eb70a4b1856625d0ebdce4cc0ce64651e2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 20 May 2022 16:04:23 -0700 Subject: [PATCH] Add 'Metadata::render()': direct template render. Resolves #1177. --- contrib/dyn_templates/src/lib.rs | 32 ++++++++------- contrib/dyn_templates/src/metadata.rs | 52 ++++++++++++++++++++++-- contrib/dyn_templates/tests/templates.rs | 45 +++++++++++++------- 3 files changed, 97 insertions(+), 32 deletions(-) diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 5410beb3..2b6519b4 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -115,10 +115,14 @@ //! //! ## Rendering //! -//! 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. The [`context!`] macro can also be used to create inline +//! Templates are typically rendered indirectly via [`Template::render()`] which +//! returns a `Template` responder which renders the template at response time. +//! To render a template directly into a `String`, use [`Metadata::render()`] +//! instead. +//! +//! Both methods take in a template name and context to use while rendering. The +//! context can be any [`Serialize`] type that serializes to an `Object` (a +//! dictionary) value. The [`context!`] macro may be used to create inline //! `Serialize`-able context objects. //! //! ## Automatic Reloading @@ -308,6 +312,8 @@ impl Template { /// be of any type that implements `Serialize`, such as `HashMap` or a /// custom `struct`. /// + /// To render a template directly into a string, use [`Metadata::render()`]. + /// /// # Examples /// /// Using the `context` macro: @@ -383,14 +389,14 @@ impl Template { None })?; - Template::render(name, context).finalize(&ctxt).ok().map(|v| v.0) + Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1) } /// Actually 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> { + fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> { let name = &*self.name; let info = ctxt.templates.get(name).ok_or_else(|| { let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect(); @@ -410,7 +416,7 @@ impl Template { Status::InternalServerError })?; - Ok((string, info.data_type.clone())) + Ok((info.data_type.clone(), string)) } } @@ -419,18 +425,16 @@ impl Template { /// rendering fails, an `Err` of `Status::InternalServerError` is returned. impl<'r> Responder<'r, 'static> for Template { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { - let (render, content_type) = { - let ctxt = req.rocket().state::().ok_or_else(|| { + let ctxt = req.rocket() + .state::() + .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 - })?.context(); + })?; - self.finalize(&ctxt)? - }; - - (content_type, render).respond_to(req) + self.finalize(&ctxt.context())?.respond_to(req) } } diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index 019a7c25..feceab36 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -1,8 +1,11 @@ -use rocket::{Request, Rocket, Ignite, Sentinel}; -use rocket::http::Status; -use rocket::request::{self, FromRequest}; +use std::borrow::Cow; -use crate::context::ContextManager; +use rocket::{Request, Rocket, Ignite, Sentinel}; +use rocket::http::{Status, ContentType}; +use rocket::request::{self, FromRequest}; +use rocket::serde::Serialize; + +use crate::{Template, context::ContextManager}; /// Request guard for dynamically querying template metadata. /// @@ -77,6 +80,47 @@ impl Metadata<'_> { pub fn reloading(&self) -> bool { self.0.is_reloading() } + + /// Directly render the template named `name` with the context `context` + /// into a `String`. Also returns the template's detected `ContentType`. See + /// [`Template::render()`] for more details on rendering. + /// + /// # Examples + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::ContentType; + /// use rocket_dyn_templates::{Metadata, Template, context}; + /// + /// #[get("/")] + /// fn send_email(metadata: Metadata) -> Option<()> { + /// let (mime, string) = metadata.render("email", context! { + /// field: "Hello, world!" + /// })?; + /// + /// # /* + /// send_email(mime, string).await?; + /// # */ + /// Some(()) + /// } + /// + /// #[get("/")] + /// fn raw_render(metadata: Metadata) -> Option<(ContentType, String)> { + /// metadata.render("index", context! { field: "Hello, world!" }) + /// } + /// + /// // Prefer the following, however, which is nearly identical but pithier: + /// + /// #[get("/")] + /// fn render() -> Template { + /// Template::render("index", context! { field: "Hello, world!" }) + /// } + /// ``` + pub fn render(&self, name: S, context: C) -> Option<(ContentType, String)> + where S: Into>, C: Serialize + { + Template::render(name.into(), context).finalize(&self.0.context()).ok() + } } impl Sentinel for Metadata<'_> { diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index 5ba42ea2..c73927f9 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -10,10 +10,7 @@ use rocket_dyn_templates::{Template, Metadata, context}; #[get("//")] fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> { - match md.contains_template(&format!("{}/{}", engine, name)) { - true => Some(()), - false => None - } + md.contains_template(&format!("{}/{}", engine, name)).then(|| ()) } #[get("/is_reloading")] @@ -207,28 +204,37 @@ fn test_context_macro() { mod tera_tests { use super::*; use std::collections::HashMap; - use rocket::http::Status; - use rocket::local::blocking::Client; + use rocket::http::{ContentType, Status}; + use rocket::request::FromRequest; const UNESCAPED_EXPECTED: &'static str = "\nh_start\ntitle: _test_\nh_end\n\n\n