use std::borrow::Cow;
use std::path::PathBuf;
use rocket::fairing::Fairing;
use rocket::figment::{error::Error, value::Value};
use rocket::http::{ContentType, Status};
use rocket::request::Request;
use rocket::response::{self, Responder};
use rocket::serde::Serialize;
use rocket::trace::Trace;
use rocket::{Ignite, Orbit, Rocket, Sentinel};
use crate::context::{Context, ContextManager};
use crate::fairing::TemplateFairing;
use crate::Engines;
pub(crate) const DEFAULT_TEMPLATE_DIR: &str = "templates";
#[derive(Debug)]
pub struct Template {
name: Cow<'static, str>,
value: Result<Value, Error>,
}
#[derive(Debug)]
pub(crate) struct TemplateInfo {
pub(crate) path: Option<PathBuf>,
pub(crate) engine_ext: &'static str,
pub(crate) data_type: ContentType,
}
impl Template {
pub fn fairing() -> impl Fairing {
Template::custom(|_| {})
}
pub fn custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
where
F: Fn(&mut Engines),
{
Self::try_custom(move |engines| {
f(engines);
Ok(())
})
}
pub fn try_custom<F: Send + Sync + 'static>(f: F) -> impl Fairing
where
F: Fn(&mut Engines) -> Result<(), Box<dyn std::error::Error>>,
{
TemplateFairing {
callback: Box::new(f),
}
}
#[inline]
pub fn render<S, C>(name: S, context: C) -> Template
where
S: Into<Cow<'static, str>>,
C: Serialize,
{
Template {
name: name.into(),
value: Value::serialize(context),
}
}
#[inline]
pub fn show<S, C>(rocket: &Rocket<Orbit>, name: S, context: C) -> Option<String>
where
S: Into<Cow<'static, str>>,
C: Serialize,
{
let ctxt = rocket
.state::<ContextManager>()
.map(ContextManager::context)
.or_else(|| {
error!(
"Uninitialized template context: missing fairing.\n\
To use templates, you must attach `Template::fairing()`.\n\
See the `Template` documentation for more information."
);
None
})?;
Template::render(name, context)
.finalize(&ctxt)
.ok()
.map(|v| v.1)
}
#[inline(always)]
pub(crate) fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> {
let template = &*self.name;
let info = ctxt.templates.get(template).ok_or_else(|| {
let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect();
error!(
%template, search_path = %ctxt.root.display(), known_templates = ?ts,
"requested template not found"
);
Status::InternalServerError
})?;
let value = self.value.map_err(|e| {
span_error!("templating", "template context failed to serialize" => e.trace_error());
Status::InternalServerError
})?;
let string = ctxt.engines.render(template, info, value).ok_or_else(|| {
error!(template, "template failed to render");
Status::InternalServerError
})?;
Ok((info.data_type.clone(), string))
}
}
impl<'r> Responder<'r, 'static> for Template {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let ctxt = req.rocket().state::<ContextManager>().ok_or_else(|| {
error!(
"uninitialized template context: missing `Template::fairing()`.\n\
To use templates, you must attach `Template::fairing()`."
);
Status::InternalServerError
})?;
self.finalize(&ctxt.context())?.respond_to(req)
}
}
impl Sentinel for Template {
fn abort(rocket: &Rocket<Ignite>) -> bool {
if rocket.state::<ContextManager>().is_none() {
error!(
"Missing `Template::fairing()`.\n\
To use templates, you must attach `Template::fairing()`."
);
return true;
}
false
}
}
#[macro_export]
macro_rules! context {
($($key:ident $(: $value:expr)?),*$(,)?) => {{
use $crate::serde::ser::{Serialize, Serializer, SerializeMap};
use ::std::fmt::{Debug, Formatter};
use ::std::result::Result;
#[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)?),*
}
}};
}