1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

Refactor templates loading & implement templates hot-reload

This commit is contained in:
Quentin Gliech
2021-12-09 14:07:09 +01:00
parent 8df4b315f2
commit c53318eca0
13 changed files with 375 additions and 102 deletions

103
Cargo.lock generated
View File

@ -319,11 +319,24 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
dependencies = [
"byteorder",
"iovec",
]
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
dependencies = [
"serde",
]
[[package]]
name = "cc"
@ -843,6 +856,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futures"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]]
name = "futures"
version = "0.3.18"
@ -931,6 +950,7 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
"futures 0.1.31",
"futures-channel",
"futures-core",
"futures-io",
@ -1014,7 +1034,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
dependencies = [
"bytes",
"bytes 1.1.0",
"fnv",
"futures-core",
"futures-sink",
@ -1073,7 +1093,7 @@ checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0"
dependencies = [
"base64 0.13.0",
"bitflags",
"bytes",
"bytes 1.1.0",
"headers-core",
"http",
"httpdate",
@ -1130,7 +1150,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
dependencies = [
"bytes",
"bytes 1.1.0",
"fnv",
"itoa",
]
@ -1141,7 +1161,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
dependencies = [
"bytes",
"bytes 1.1.0",
"http",
"pin-project-lite",
]
@ -1176,7 +1196,7 @@ version = "0.14.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c"
dependencies = [
"bytes",
"bytes 1.1.0",
"futures-channel",
"futures-core",
"futures-util",
@ -1294,6 +1314,15 @@ version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dc51180a9b377fd75814d0cc02199c20f8e99433d6762f650d39cdbbd3b56f"
[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.3.1"
@ -1435,7 +1464,7 @@ dependencies = [
"argon2",
"clap",
"dotenv",
"futures",
"futures 0.3.18",
"hyper",
"indoc",
"mas-config",
@ -1458,6 +1487,7 @@ dependencies = [
"tracing-subscriber",
"url",
"warp",
"watchman_client",
]
[[package]]
@ -1826,7 +1856,7 @@ dependencies = [
"crossbeam-channel 0.5.1",
"dashmap",
"fnv",
"futures",
"futures 0.3.18",
"js-sys",
"lazy_static",
"percent-encoding",
@ -1844,7 +1874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50ceb0b0e8b75cb3e388a2571a807c8228dabc5d6670f317b6eb21301095373"
dependencies = [
"async-trait",
"bytes",
"bytes 1.1.0",
"futures-util",
"http",
"opentelemetry",
@ -1877,7 +1907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f19d4b43842433c420c548c985d158f5628bba5b518e0be64627926d19889992"
dependencies = [
"async-trait",
"futures",
"futures 0.3.18",
"http",
"opentelemetry",
"prost",
@ -2265,7 +2295,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020"
dependencies = [
"bytes",
"bytes 1.1.0",
"prost-derive",
]
@ -2275,7 +2305,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355f634b43cdd80724ee7848f95770e7e70eefa6dcf14fea676216573b8fd603"
dependencies = [
"bytes",
"bytes 1.1.0",
"heck",
"itertools",
"log",
@ -2306,7 +2336,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "603bbd6394701d13f3f25aada59c7de9d35a6a5887cfc156181234a44002771b"
dependencies = [
"bytes",
"bytes 1.1.0",
"prost",
]
@ -2432,7 +2462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bea77bc708afa10e59905c3d4af7c8fd43c9214251673095ff8b14345fcbc5"
dependencies = [
"base64 0.13.0",
"bytes",
"bytes 1.1.0",
"encoding_rs",
"futures-core",
"futures-util",
@ -2695,6 +2725,19 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bser"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b929ea725591083cbca8b8ea178ed6efc918eccd40b784e199ce88967104199"
dependencies = [
"anyhow",
"byteorder",
"bytes 0.4.12",
"serde",
"thiserror",
]
[[package]]
name = "serde_cbor"
version = "0.11.2"
@ -2944,7 +2987,7 @@ dependencies = [
"base64 0.13.0",
"bitflags",
"byteorder",
"bytes",
"bytes 1.1.0",
"chrono",
"crc",
"crossbeam-channel 0.5.1",
@ -3327,7 +3370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg 1.0.1",
"bytes",
"bytes 1.1.0",
"libc",
"memchr",
"mio",
@ -3413,11 +3456,13 @@ version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
dependencies = [
"bytes",
"bytes 1.1.0",
"futures-core",
"futures-io",
"futures-sink",
"log",
"pin-project-lite",
"slab",
"tokio",
]
@ -3430,7 +3475,7 @@ dependencies = [
"async-stream",
"async-trait",
"base64 0.13.0",
"bytes",
"bytes 1.1.0",
"futures-core",
"futures-util",
"h2",
@ -3495,7 +3540,7 @@ dependencies = [
"async-compression",
"base64 0.13.0",
"bitflags",
"bytes",
"bytes 1.1.0",
"futures-core",
"futures-util",
"http",
@ -3625,7 +3670,7 @@ checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes",
"bytes 1.1.0",
"http",
"httparse",
"log",
@ -3855,7 +3900,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e"
dependencies = [
"bytes",
"bytes 1.1.0",
"futures-channel",
"futures-util",
"headers",
@ -3951,6 +3996,24 @@ version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "watchman_client"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1afbab1186833c9b34f64132b80ed4b373ed4eab6f9efa1f55430835200f0a28"
dependencies = [
"anyhow",
"bytes 1.1.0",
"futures 0.3.18",
"maplit",
"serde",
"serde_bser",
"thiserror",
"tokio",
"tokio-util",
"winapi",
]
[[package]]
name = "web-sys"
version = "0.3.55"

View File

@ -34,12 +34,14 @@ opentelemetry-zipkin = { version = "0.14.0", features = ["reqwest-client", "reqw
mas-config = { path = "../config" }
mas-core = { path = "../core" }
mas-templates = { path = "../templates" }
watchman_client = "0.7.1"
[dev-dependencies]
indoc = "1.0.3"
[features]
default = ["otlp", "jaeger", "zipkin"]
dev = ["mas-templates/dev"]
# Enable OpenTelemetry OTLP exporter. Requires "protoc"
otlp = ["opentelemetry-otlp"]
# Enable OpenTelemetry Jaeger exporter and propagator.

View File

@ -19,6 +19,7 @@ use std::{
use anyhow::Context;
use clap::Parser;
use futures::{future::TryFutureExt, stream::TryStreamExt};
use hyper::{header, Server, Version};
use mas_config::RootConfig;
use mas_core::{
@ -33,7 +34,7 @@ use tower_http::{
sensitive_headers::SetSensitiveHeadersLayer,
trace::{MakeSpan, OnResponse, TraceLayer},
};
use tracing::{field, info};
use tracing::{error, field, info};
use super::RootCommand;
@ -42,6 +43,10 @@ pub(super) struct ServerCommand {
/// Automatically apply pending migrations
#[clap(long)]
migrate: bool,
/// Watch for changes for templates on the filesystem
#[clap(short, long)]
watch: bool,
}
#[derive(Debug, Clone, Default)]
@ -136,6 +141,76 @@ async fn shutdown_signal() {
};
}
/// Watch for changes in the templates folders
async fn watch_templates(
client: &watchman_client::Client,
templates: &Templates,
) -> anyhow::Result<()> {
use watchman_client::{
fields::NameOnly,
pdu::{QueryResult, SubscribeRequest},
CanonicalPath, SubscriptionData,
};
let templates = templates.clone();
// Find which roots we're supposed to watch
let roots = templates.watch_roots().await;
let mut streams = Vec::new();
for root in roots {
// For each root, create a subscription
let resolved = client
.resolve_root(CanonicalPath::canonicalize(root)?)
.await?;
// TODO: we could subscribe to less, properly filter here
let (subscription, _) = client
.subscribe::<NameOnly>(&resolved, SubscribeRequest::default())
.await?;
// Create a stream out of that subscription
let stream = futures::stream::try_unfold(subscription, |mut sub| async move {
let next = sub.next().await?;
anyhow::Ok(Some((next, sub)))
});
streams.push(Box::pin(stream));
}
let files_changed_stream =
futures::stream::select_all(streams).try_filter_map(|event| async move {
match event {
SubscriptionData::FilesChanged(QueryResult {
files: Some(files), ..
}) => {
let files: Vec<_> = files.into_iter().map(|f| f.name.into_inner()).collect();
Ok(Some(files))
}
_ => Ok(None),
}
});
let fut = files_changed_stream
.try_for_each(move |files| {
let templates = templates.clone();
async move {
info!(?files, "Files changed, reloading templates");
templates
.clone()
.reload()
.await
.context("Could not reload templates")
}
})
.inspect_err(|err| error!(%err, "Error while watching templates, stop watching"));
tokio::spawn(fut);
Ok(())
}
impl ServerCommand {
pub async fn run(&self, root: &RootCommand) -> anyhow::Result<()> {
let config: RootConfig = root.load_config()?;
@ -160,8 +235,21 @@ impl ServerCommand {
queue.start();
// Load and compile the templates
let templates =
Templates::load_from_config(&config.templates).context("could not load templates")?;
let templates = Templates::load_from_config(&config.templates)
.await
.context("could not load templates")?;
// Watch for changes in templates if the --watch flag is present
if self.watch {
let client = watchman_client::Connector::new()
.connect()
.await
.context("could not connect to watchman")?;
watch_templates(&client, &templates)
.await
.context("could not watch for templates changes")?;
}
// Start the server
let root = mas_core::handlers::root(&pool, &templates, &config);

View File

@ -15,6 +15,7 @@
use std::path::PathBuf;
use clap::Parser;
use mas_config::TemplatesConfig;
use mas_templates::Templates;
use super::RootCommand;
@ -59,8 +60,12 @@ impl TemplatesCommand {
}
SC::Check { path, skip_builtin } => {
let templates = Templates::load(Some(path), !skip_builtin)?;
templates.check_render()?;
let config = TemplatesConfig {
path: Some(path.to_string()),
builtin: !skip_builtin,
};
let templates = Templates::load_from_config(&config).await?;
templates.check_render().await?;
Ok(())
}

View File

@ -22,7 +22,7 @@ fn default_builtin() -> bool {
true
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct TemplatesConfig {
/// Path to the folder that holds the custom templates
#[serde(default)]

View File

@ -96,7 +96,7 @@ enum ReplyOrBackToClient {
Error(Box<dyn OAuth2Error>),
}
fn back_to_client<T>(
async fn back_to_client<T>(
mut redirect_uri: Url,
response_mode: ResponseMode,
state: Option<String>,
@ -175,7 +175,7 @@ where
ResponseMode::FormPost => {
let merged = ParamsWithState { state, params };
let ctx = FormPostContext::new(redirect_uri, merged);
let rendered = templates.render_form_post(&ctx)?;
let rendered = templates.render_form_post(&ctx).await?;
Ok(Box::new(html(rendered)))
}
}
@ -288,19 +288,19 @@ async fn actually_reply(
let client = match client {
Some(client) => client,
None => return Ok(Box::new(html(templates.render_error(&error.into())?))),
None => return Ok(Box::new(html(templates.render_error(&error.into()).await?))),
};
let redirect_uri: Result<Option<Url>, _> = redirect_uri.map(|r| r.parse()).transpose();
let redirect_uri = match redirect_uri {
Ok(r) => r,
Err(_) => return Ok(Box::new(html(templates.render_error(&error.into())?))),
Err(_) => return Ok(Box::new(html(templates.render_error(&error.into()).await?))),
};
let redirect_uri = client.resolve_redirect_uri(&redirect_uri);
let redirect_uri = match redirect_uri {
Ok(r) => r,
Err(_) => return Ok(Box::new(html(templates.render_error(&error.into())?))),
Err(_) => return Ok(Box::new(html(templates.render_error(&error.into()).await?))),
};
let reply: ErrorResponse = error.into();
@ -310,7 +310,9 @@ async fn actually_reply(
}
};
back_to_client(redirect_uri, response_mode, state, params, &templates).wrap_error()
back_to_client(redirect_uri, response_mode, state, params, &templates)
.await
.wrap_error()
}
async fn get(

View File

@ -58,7 +58,7 @@ async fn get(
.maybe_with_session(maybe_session)
.with_csrf(csrf_token.form_value());
let content = templates.render_index(&ctx)?;
let content = templates.render_index(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))

View File

@ -138,7 +138,7 @@ async fn get(
None => ctx,
};
let ctx = ctx.with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx)?;
let content = templates.render_login(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))
@ -171,7 +171,7 @@ async fn post(
let ctx = LoginContext::default()
.with_form_error(errored_form)
.with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx)?;
let content = templates.render_login(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))

View File

@ -132,7 +132,7 @@ async fn get(
};
let ctx = ctx.with_session(session).with_csrf(csrf_token.form_value());
let content = templates.render_reauth(&ctx)?;
let content = templates.render_reauth(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(reply)

View File

@ -117,7 +117,7 @@ async fn get(
Ok(Box::new(query.redirect()?))
} else {
let ctx = EmptyContext.with_csrf(csrf_token.form_value());
let content = templates.render_register(&ctx)?;
let content = templates.render_register(&ctx).await?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
Ok(Box::new(reply))

View File

@ -5,6 +5,9 @@ authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2018"
license = "Apache-2.0"
[features]
dev = []
[dependencies]
tracing = "0.1.29"
tokio = "1.14.0"

View File

@ -23,14 +23,20 @@
//! Templates rendering
use std::{collections::HashSet, io::Cursor, path::Path, string::ToString, sync::Arc};
use std::{
collections::HashSet,
io::Cursor,
path::{Path, PathBuf},
string::ToString,
sync::Arc,
};
use anyhow::Context as _;
use anyhow::{bail, Context as _};
use mas_config::TemplatesConfig;
use serde::Serialize;
use tera::{Context, Error as TeraError, Tera};
use thiserror::Error;
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
use tokio::{fs::OpenOptions, io::AsyncWriteExt, sync::RwLock, task::JoinError};
use tracing::{debug, info, warn};
#[allow(missing_docs)] // TODO
@ -47,7 +53,10 @@ pub use self::context::{
/// Wrapper around [`tera::Tera`] helping rendering the various templates
#[derive(Debug, Clone)]
pub struct Templates(Arc<Tera>);
pub struct Templates {
tera: Arc<RwLock<Tera>>,
config: TemplatesConfig,
}
/// There was an issue while loading the templates
#[derive(Error, Debug)]
@ -56,6 +65,10 @@ pub enum TemplateLoadingError {
#[error("could not load and compile some templates")]
Compile(#[from] TeraError),
/// Could not join blocking task
#[error("error from async runtime")]
Runtime(#[from] JoinError),
/// There are essential templates missing
#[error("missing templates {missing:?}")]
MissingTemplates {
@ -67,45 +80,105 @@ pub enum TemplateLoadingError {
}
impl Templates {
/// Load the templates from [the config][`TemplatesConfig`]
pub fn load_from_config(config: &TemplatesConfig) -> Result<Self, TemplateLoadingError> {
Self::load(config.path.as_deref(), config.builtin)
/// List directories to watch
pub async fn watch_roots(&self) -> Vec<PathBuf> {
Self::roots(self.config.path.as_deref(), self.config.builtin)
.await
.into_iter()
.filter_map(Result::ok)
.collect()
}
/// Load the templates and check all needed templates are properly loaded
///
/// # Arguments
///
/// * `path` - An optional path to where templates should be loaded
/// * `builtin` - Set to `true` to load the builtin templates as well
pub fn load(path: Option<&str>, builtin: bool) -> Result<Self, TemplateLoadingError> {
let tera = {
let mut tera = Tera::default();
if builtin {
info!("Loading builtin templates");
for (name, source) in EXTRA_TEMPLATES {
tera.add_raw_template(name, source)?;
}
for (name, source) in TEMPLATES {
tera.add_raw_template(name, source)?;
}
async fn roots(path: Option<&str>, builtin: bool) -> Vec<Result<PathBuf, std::io::Error>> {
let mut paths = Vec::new();
if builtin && cfg!(feature = "dev") {
paths.push(PathBuf::from(format!(
"{}/src/res",
env!("CARGO_MANIFEST_DIR")
)));
}
if let Some(path) = path {
let path = format!("{}/**/*.{{html,txt}}", path);
info!(%path, "Loading templates from filesystem");
tera.extend(&Tera::parse(&path)?)?;
paths.push(PathBuf::from(path));
}
let mut ret = Vec::new();
for path in paths {
ret.push(tokio::fs::read_dir(&path).await.map(|_| path));
}
ret
}
fn load_builtin() -> Result<Tera, TemplateLoadingError> {
let mut tera = Tera::default();
info!("Loading builtin templates");
for (name, source) in EXTRA_TEMPLATES {
if let Some(source) = source {
tera.add_raw_template(name, source)?;
}
}
for (name, source) in TEMPLATES {
if let Some(source) = source {
tera.add_raw_template(name, source)?;
}
}
Ok(tera)
}
/// Load the templates from [the config][`TemplatesConfig`]
pub async fn load_from_config(config: &TemplatesConfig) -> Result<Self, TemplateLoadingError> {
let tera = Self::load(config.path.as_deref(), config.builtin).await?;
Ok(Self {
tera: Arc::new(RwLock::new(tera)),
config: config.clone(),
})
}
async fn load(path: Option<&str>, builtin: bool) -> Result<Tera, TemplateLoadingError> {
let mut teras = Vec::new();
let roots = Self::roots(path, builtin).await;
for maybe_root in roots {
let root = match maybe_root {
Ok(root) => root,
Err(err) => {
warn!(%err, "Could not open a template folder, skipping it");
continue;
}
};
// This uses blocking I/Os, do that in a blocking task
let tera = tokio::task::spawn_blocking(move || {
// Using `to_string_lossy` here is probably fine
let path = format!("{}/**/*.{{html,txt}}", root.to_string_lossy());
info!(%path, "Loading templates from filesystem");
Tera::parse(&path)
})
.await??;
teras.push(tera);
}
if builtin {
teras.push(Self::load_builtin()?);
}
// Merging all Tera instances into a single one
let mut tera = teras
.into_iter()
.try_fold(Tera::default(), |mut acc, tera| {
acc.extend(&tera)?;
Ok::<_, TemplateLoadingError>(acc)
})?;
tera.build_inheritance_chains()?;
tera.check_macro_files()?;
tera
};
let loaded: HashSet<_> = tera.get_template_names().collect();
let needed: HashSet<_> = std::array::IntoIter::new(TEMPLATES)
.map(|(name, _)| name)
@ -114,7 +187,7 @@ impl Templates {
let missing: HashSet<_> = needed.difference(&loaded).collect();
if missing.is_empty() {
Ok(Self(Arc::new(tera)))
Ok(tera)
} else {
let missing = missing.into_iter().map(ToString::to_string).collect();
let loaded = loaded.into_iter().map(ToString::to_string).collect();
@ -122,8 +195,23 @@ impl Templates {
}
}
/// Reload the templates on disk
pub async fn reload(&self) -> anyhow::Result<()> {
// Prepare the new Tera instance
let new_tera = Self::load(self.config.path.as_deref(), self.config.builtin).await?;
// Swap it
*self.tera.write().await = new_tera;
Ok(())
}
/// Save the builtin templates to a folder
pub async fn save(path: &Path, overwrite: bool) -> anyhow::Result<()> {
if cfg!(feature = "dev") {
bail!("Builtin templates are not included in dev binaries")
}
tokio::fs::create_dir_all(&path)
.await
.context("could not create destination folder")?;
@ -140,6 +228,7 @@ impl Templates {
};
for (name, source) in templates {
if let Some(source) = source {
let path = path.join(name);
let mut file = match options.open(&path).await {
@ -157,6 +246,7 @@ impl Templates {
.context(format!("could not write file {:?}", path))?;
info!(?path, "Wrote template");
}
}
Ok(())
}
@ -215,13 +305,13 @@ register_templates! {
impl Templates {
/// Render all templates with the generated samples to check if they render
/// properly
pub fn check_render(&self) -> anyhow::Result<()> {
check::render_login(self)?;
check::render_register(self)?;
check::render_index(self)?;
check::render_reauth(self)?;
check::render_form_post::<EmptyContext>(self)?;
check::render_error(self)?;
pub async fn check_render(&self) -> anyhow::Result<()> {
check::render_login(self).await?;
check::render_register(self).await?;
check::render_index(self).await?;
check::render_reauth(self).await?;
check::render_form_post::<EmptyContext>(self).await?;
check::render_error(self).await?;
Ok(())
}
}
@ -230,9 +320,14 @@ impl Templates {
mod tests {
use super::*;
#[test]
fn check_builtin_templates() {
let templates = Templates::load(None, true).unwrap();
templates.check_render().unwrap();
#[tokio::test]
async fn check_builtin_templates() {
let config = TemplatesConfig {
path: None,
builtin: true,
};
let templates = Templates::load_from_config(&config).await.unwrap();
templates.check_render().await.unwrap();
}
}

View File

@ -49,26 +49,40 @@ macro_rules! register_templates {
)*
} => {
/// List of registered templates
static TEMPLATES: [(&'static str, &'static str); count!( $( $template )* )] = [
$( ($template, include_str!(concat!("res/", $template))) ),*
static TEMPLATES: [(&'static str, Option<&'static str>); count!( $( $template )* )] = [
$( (
$template,
if cfg!(feature = "dev") {
None
} else {
Some(include_str!(concat!("res/", $template)))
}
) ),*
];
/// List of extra templates used by other templates
static EXTRA_TEMPLATES: [(&'static str, &'static str); count!( $( $( $extra_template )* )? )] = [
$( $( ($extra_template, include_str!(concat!("res/", $extra_template))) ),* )?
static EXTRA_TEMPLATES: [(&'static str, Option<&'static str>); count!( $( $( $extra_template )* )? )] = [
$( $( (
$extra_template,
if cfg!(feature = "dev") {
None
} else {
Some(include_str!(concat!("res/", $extra_template)))
}
) ),* )?
];
impl Templates {
$(
$(#[$attr])?
pub fn $name
pub async fn $name
$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
(&self, context: &$param)
-> Result<String, TemplateError> {
let ctx = Context::from_serialize(context)
.map_err(|source| TemplateError::Context { template: $template, source })?;
self.0.render($template, &ctx)
self.tera.read().await.render($template, &ctx)
.map_err(|source| TemplateError::Render { template: $template, source })
}
)*
@ -80,7 +94,7 @@ macro_rules! register_templates {
$(
#[doc = concat!("Render the `", $template, "` template with sample contexts")]
pub fn $name
pub async fn $name
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
(templates: &Templates)
-> anyhow::Result<()> {
@ -91,6 +105,7 @@ macro_rules! register_templates {
let context = serde_json::to_value(&sample)?;
::tracing::info!(name, %context, "Rendering template");
templates. $name (&sample)
.await
.with_context(|| format!("Failed to render template {:?} with context {}", name, context))?;
}