Add 'Metadata::render()': direct template render.

Resolves #1177.
This commit is contained in:
Sergio Benitez 2022-05-20 16:04:23 -07:00
parent 4827948401
commit 9b3c83eb70
3 changed files with 97 additions and 32 deletions

View File

@ -115,10 +115,14 @@
//! //!
//! ## Rendering //! ## Rendering
//! //!
//! Templates are rendered with the `render` method. The method takes in the //! Templates are typically rendered indirectly via [`Template::render()`] which
//! name of a template and a context to render the template with. The context //! returns a `Template` responder which renders the template at response time.
//! can be any type that implements [`Serialize`] and would serialize to an //! To render a template directly into a `String`, use [`Metadata::render()`]
//! `Object` value. The [`context!`] macro can also be used to create inline //! 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. //! `Serialize`-able context objects.
//! //!
//! ## Automatic Reloading //! ## Automatic Reloading
@ -308,6 +312,8 @@ impl Template {
/// be of any type that implements `Serialize`, such as `HashMap` or a /// be of any type that implements `Serialize`, such as `HashMap` or a
/// custom `struct`. /// custom `struct`.
/// ///
/// To render a template directly into a string, use [`Metadata::render()`].
///
/// # Examples /// # Examples
/// ///
/// Using the `context` macro: /// Using the `context` macro:
@ -383,14 +389,14 @@ impl Template {
None 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 /// Actually render this template given a template context. This method is
/// called by the `Template` `Responder` implementation as well as /// called by the `Template` `Responder` implementation as well as
/// `Template::show()`. /// `Template::show()`.
#[inline(always)] #[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 name = &*self.name;
let info = ctxt.templates.get(name).ok_or_else(|| { let info = ctxt.templates.get(name).ok_or_else(|| {
let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect(); let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
@ -410,7 +416,7 @@ impl Template {
Status::InternalServerError 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. /// rendering fails, an `Err` of `Status::InternalServerError` is returned.
impl<'r> Responder<'r, 'static> for Template { impl<'r> Responder<'r, 'static> for Template {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let (render, content_type) = { let ctxt = req.rocket()
let ctxt = req.rocket().state::<ContextManager>().ok_or_else(|| { .state::<ContextManager>()
.ok_or_else(|| {
error_!("Uninitialized template context: missing fairing."); error_!("Uninitialized template context: missing fairing.");
info_!("To use templates, you must attach `Template::fairing()`."); info_!("To use templates, you must attach `Template::fairing()`.");
info_!("See the `Template` documentation for more information."); info_!("See the `Template` documentation for more information.");
Status::InternalServerError Status::InternalServerError
})?.context(); })?;
self.finalize(&ctxt)? self.finalize(&ctxt.context())?.respond_to(req)
};
(content_type, render).respond_to(req)
} }
} }

View File

@ -1,8 +1,11 @@
use rocket::{Request, Rocket, Ignite, Sentinel}; use std::borrow::Cow;
use rocket::http::Status;
use rocket::request::{self, FromRequest};
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. /// Request guard for dynamically querying template metadata.
/// ///
@ -77,6 +80,47 @@ impl Metadata<'_> {
pub fn reloading(&self) -> bool { pub fn reloading(&self) -> bool {
self.0.is_reloading() 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<S, C>(&self, name: S, context: C) -> Option<(ContentType, String)>
where S: Into<Cow<'static, str>>, C: Serialize
{
Template::render(name.into(), context).finalize(&self.0.context()).ok()
}
} }
impl Sentinel for Metadata<'_> { impl Sentinel for Metadata<'_> {

View File

@ -10,10 +10,7 @@ use rocket_dyn_templates::{Template, Metadata, context};
#[get("/<engine>/<name>")] #[get("/<engine>/<name>")]
fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> { fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> {
match md.contains_template(&format!("{}/{}", engine, name)) { md.contains_template(&format!("{}/{}", engine, name)).then(|| ())
true => Some(()),
false => None
}
} }
#[get("/is_reloading")] #[get("/is_reloading")]
@ -207,28 +204,37 @@ fn test_context_macro() {
mod tera_tests { mod tera_tests {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::http::Status; use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client; use rocket::request::FromRequest;
const UNESCAPED_EXPECTED: &'static str const UNESCAPED_EXPECTED: &'static str
= "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot\n"; = "\nh_start\ntitle: _test_\nh_end\n\n\n<script />\n\nfoot\n";
const ESCAPED_EXPECTED: &'static str const ESCAPED_EXPECTED: &'static str
= "\nh_start\ntitle: _test_\nh_end\n\n\n&lt;script &#x2F;&gt;\n\nfoot\n"; = "\nh_start\ntitle: _test_\nh_end\n\n\n&lt;script &#x2F;&gt;\n\nfoot\n";
#[test] #[async_test]
fn test_tera_templates() { async fn test_tera_templates() {
let client = Client::debug(rocket()).unwrap(); use rocket::local::asynchronous::Client;
let client = Client::debug(rocket()).await.unwrap();
let req = client.get("/");
let metadata = Metadata::from_request(&req).await.unwrap();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("title", "_test_"); map.insert("title", "_test_");
map.insert("content", "<script />"); map.insert("content", "<script />");
// Test with a txt file, which shouldn't escape. // Test with a txt file, which shouldn't escape.
let template = Template::show(client.rocket(), "tera/txt_test", &map); let template = Template::show(client.rocket(), "tera/txt_test", &map);
let md_rendered = metadata.render("tera/txt_test", &map);
assert_eq!(template, Some(UNESCAPED_EXPECTED.into())); assert_eq!(template, Some(UNESCAPED_EXPECTED.into()));
assert_eq!(md_rendered, Some((ContentType::Text, UNESCAPED_EXPECTED.into())));
// Now with an HTML file, which should. // Now with an HTML file, which should.
let template = Template::show(client.rocket(), "tera/html_test", &map); let template = Template::show(client.rocket(), "tera/html_test", &map);
let md_rendered = metadata.render("tera/html_test", &map);
assert_eq!(template, Some(ESCAPED_EXPECTED.into())); assert_eq!(template, Some(ESCAPED_EXPECTED.into()));
assert_eq!(md_rendered, Some((ContentType::HTML, ESCAPED_EXPECTED.into())));
} }
// u128 is not supported. enable when it is. // u128 is not supported. enable when it is.
@ -248,6 +254,8 @@ mod tera_tests {
#[test] #[test]
fn test_template_metadata_with_tera() { fn test_template_metadata_with_tera() {
use rocket::local::blocking::Client;
let client = Client::debug(rocket()).unwrap(); let client = Client::debug(rocket()).unwrap();
let response = client.get("/tera/txt_test").dispatch(); let response = client.get("/tera/txt_test").dispatch();
@ -268,22 +276,29 @@ mod tera_tests {
mod handlebars_tests { mod handlebars_tests {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
use rocket::http::Status; use rocket::request::FromRequest;
use rocket::local::blocking::Client; use rocket::http::{ContentType, Status};
#[async_test]
async fn test_handlebars_templates() {
use rocket::local::asynchronous::Client;
#[test]
fn test_handlebars_templates() {
const EXPECTED: &'static str const EXPECTED: &'static str
= "Hello _test_!\n<main> &lt;script /&gt; hi </main>\nDone.\n"; = "Hello _test_!\n<main> &lt;script /&gt; hi </main>\nDone.\n";
let client = Client::debug(rocket()).unwrap(); let client = Client::debug(rocket()).await.unwrap();
let req = client.get("/");
let metadata = Metadata::from_request(&req).await.unwrap();
let mut map = HashMap::new(); let mut map = HashMap::new();
map.insert("title", "_test_"); map.insert("title", "_test_");
map.insert("content", "<script /> hi"); map.insert("content", "<script /> hi");
// Test with a txt file, which shouldn't escape. // Test with a txt file, which shouldn't escape.
let template = Template::show(client.rocket(), "hbs/test", &map); let template = Template::show(client.rocket(), "hbs/test", &map);
let md_rendered = metadata.render("hbs/test", &map);
assert_eq!(template, Some(EXPECTED.into())); assert_eq!(template, Some(EXPECTED.into()));
assert_eq!(md_rendered, Some((ContentType::HTML, EXPECTED.into())));
} }
// u128 is not supported. enable when it is. // u128 is not supported. enable when it is.
@ -303,6 +318,8 @@ mod handlebars_tests {
#[test] #[test]
fn test_template_metadata_with_handlebars() { fn test_template_metadata_with_handlebars() {
use rocket::local::blocking::Client;
let client = Client::debug(rocket()).unwrap(); let client = Client::debug(rocket()).unwrap();
let response = client.get("/hbs/test").dispatch(); let response = client.get("/hbs/test").dispatch();