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
//!
//! 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::<ContextManager>().ok_or_else(|| {
let ctxt = req.rocket()
.state::<ContextManager>()
.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)
}
}

View File

@ -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<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<'_> {

View File

@ -10,10 +10,7 @@ use rocket_dyn_templates::{Template, Metadata, context};
#[get("/<engine>/<name>")]
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<script />\n\nfoot\n";
const ESCAPED_EXPECTED: &'static str
= "\nh_start\ntitle: _test_\nh_end\n\n\n&lt;script &#x2F;&gt;\n\nfoot\n";
#[test]
fn test_tera_templates() {
let client = Client::debug(rocket()).unwrap();
#[async_test]
async fn test_tera_templates() {
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();
map.insert("title", "_test_");
map.insert("content", "<script />");
// Test with a txt file, which shouldn't escape.
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!(md_rendered, Some((ContentType::Text, UNESCAPED_EXPECTED.into())));
// Now with an HTML file, which should.
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!(md_rendered, Some((ContentType::HTML, ESCAPED_EXPECTED.into())));
}
// u128 is not supported. enable when it is.
@ -248,6 +254,8 @@ mod tera_tests {
#[test]
fn test_template_metadata_with_tera() {
use rocket::local::blocking::Client;
let client = Client::debug(rocket()).unwrap();
let response = client.get("/tera/txt_test").dispatch();
@ -268,22 +276,29 @@ mod tera_tests {
mod handlebars_tests {
use super::*;
use std::collections::HashMap;
use rocket::http::Status;
use rocket::local::blocking::Client;
use rocket::request::FromRequest;
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
= "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();
map.insert("title", "_test_");
map.insert("content", "<script /> hi");
// Test with a txt file, which shouldn't escape.
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!(md_rendered, Some((ContentType::HTML, EXPECTED.into())));
}
// u128 is not supported. enable when it is.
@ -303,6 +318,8 @@ mod handlebars_tests {
#[test]
fn test_template_metadata_with_handlebars() {
use rocket::local::blocking::Client;
let client = Client::debug(rocket()).unwrap();
let response = client.get("/hbs/test").dispatch();