use std::any::Any;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use camel_api::CamelError;
use camel_component_api::RuntimeObservability;
use camel_component_api::template_reload::{TemplateReloadStaged, TemplateReloadTarget};
use camel_language_api::MinijinjaLimitsConfig;
use crate::closure;
use crate::config::ResolvedExternalTemplateLimits;
use crate::error::TemplateReloadError;
use crate::path_util::OwnedHandle;
use crate::template_set::{SharedTemplates, TemplateSet};
pub(crate) struct ReloadHandler {
pub(crate) shared: SharedTemplates,
pub(crate) entry_abs_path: PathBuf,
pub(crate) render_limits: MinijinjaLimitsConfig,
pub(crate) limits: ResolvedExternalTemplateLimits,
pub(crate) generation: Mutex<u64>,
pub(crate) root: Arc<OwnedHandle>,
pub(crate) rt: Option<Arc<dyn RuntimeObservability>>,
pub(crate) route_id: String,
}
impl std::fmt::Debug for ReloadHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReloadHandler")
.field("entry_abs_path", &self.entry_abs_path)
.field("route_id", &self.route_id)
.field("generation", &self.generation)
.field("rt_set", &self.rt.is_some())
.finish_non_exhaustive()
}
}
#[allow(dead_code)] pub(crate) struct StagedSet {
set: TemplateSet,
read_generation: u64,
}
impl TemplateReloadStaged for StagedSet {
fn into_any(self: Box<Self>) -> Box<dyn Any> {
self
}
}
impl ReloadHandler {
pub async fn build(&self) -> Result<(Box<dyn TemplateReloadStaged>, u64), CamelError> {
let root = Arc::clone(&self.root);
let entry = self.entry_abs_path.clone();
let limits = self.limits;
let render_limits = self.render_limits.clone();
let join =
tokio::task::spawn_blocking(move || -> Result<TemplateSet, TemplateReloadError> {
let snapshot = closure::build_snapshot(&entry, root.as_ref(), limits)?;
let entry_str = entry
.file_name()
.ok_or_else(|| {
TemplateReloadError::PathEscape("entry has no file name".into())
})?
.to_string_lossy()
.into_owned();
TemplateSet::compile(&snapshot, &entry_str, render_limits)
});
let set = join
.await
.map_err(|e| {
CamelError::TemplateReload(format!("reload build spawn_blocking join error: {e}"))
})?
.map_err(CamelError::from)?;
let read_generation = *self.generation.lock().expect("generation poisoned"); Ok((
Box::new(StagedSet {
set,
read_generation,
}),
read_generation,
))
}
pub fn current_generation(&self) -> u64 {
*self.generation.lock().expect("generation poisoned") }
pub fn commit(&self, staged: Box<dyn TemplateReloadStaged>) {
let concrete = staged
.into_any()
.downcast::<StagedSet>()
.expect("staged type matches its builder"); *self.generation.lock().expect("generation poisoned") += 1; self.shared.store(Arc::new(concrete.set));
}
}
#[async_trait]
impl TemplateReloadTarget for ReloadHandler {
fn route_id(&self) -> &str {
&self.route_id
}
fn reload_timeout(&self) -> Duration {
Duration::from_millis(self.limits.reload_timeout_ms)
}
fn current_generation(&self) -> u64 {
ReloadHandler::current_generation(self)
}
async fn build(&self) -> Result<(Box<dyn TemplateReloadStaged>, u64), CamelError> {
ReloadHandler::build(self).await
}
fn commit(&self, staged: Box<dyn TemplateReloadStaged>) {
ReloadHandler::commit(self, staged);
}
}
#[cfg(test)]
mod tests {
use super::{ReloadHandler, SharedTemplates};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use arc_swap::ArcSwap;
use camel_language_api::MinijinjaLimitsConfig;
use camel_language_minijinja::ResolvedLimits;
use crate::closure;
use crate::config::ResolvedExternalTemplateLimits;
use crate::path_util::OwnedHandle;
use crate::template_set::TemplateSet;
fn default_limits() -> ResolvedExternalTemplateLimits {
ResolvedExternalTemplateLimits {
max_total_source_bytes: 1024 * 1024,
max_include_count: 64,
max_include_depth: 16,
max_template_size: 1024 * 1024,
reload_timeout_ms: 5000,
}
}
fn ctx_name(k: &str) -> minijinja::Value {
let mut m: BTreeMap<&str, &str> = BTreeMap::new();
m.insert("name", k);
minijinja::Value::from_serialize(&m)
}
#[allow(clippy::type_complexity)]
fn seed_handler(
entry_content: &str,
) -> (
tempfile::TempDir,
Arc<ReloadHandler>,
SharedTemplates,
PathBuf,
) {
let dir = tempfile::tempdir().expect("tempdir");
let entry = dir.path().join("page.html");
fs::write(&entry, entry_content).expect("write entry");
let (root, _id) = crate::path_util::open_root(dir.path()).expect("open root");
let root: Arc<OwnedHandle> = Arc::new(root);
let snapshot = closure::build_snapshot(&entry, root.as_ref(), default_limits())
.expect("seed snapshot");
let entry_str = entry
.file_name()
.expect("entry file name")
.to_string_lossy()
.into_owned();
let set = TemplateSet::compile(&snapshot, &entry_str, MinijinjaLimitsConfig::default())
.expect("seed compile");
let shared: SharedTemplates = Arc::new(ArcSwap::from_pointee(set));
let handler = Arc::new(ReloadHandler {
shared: Arc::clone(&shared),
entry_abs_path: entry.clone(),
render_limits: MinijinjaLimitsConfig::default(),
limits: default_limits(),
generation: Mutex::new(0),
root: Arc::clone(&root),
rt: None,
route_id: "test-reload".to_string(),
});
(dir, handler, shared, entry)
}
#[tokio::test]
async fn reload_build_does_not_store_on_compile_error() {
let v1 = r#"{% autoescape "none" %}v1{{name}}{% endautoescape %}"#;
let (_dir, handler, shared, entry) = seed_handler(v1);
let gen_before = handler.current_generation();
fs::write(&entry, "broken {{name}").expect("write broken source");
let result = handler.build().await;
assert!(result.is_err(), "build of invalid source must return Err");
assert_eq!(handler.current_generation(), gen_before);
let set = shared.load_full();
let rendered = set
.render_entry(ctx_name("k"), ResolvedLimits::default())
.await
.expect("render S0");
assert_eq!(rendered, "v1k");
}
#[tokio::test]
async fn reload_commit_swaps_on_valid_change() {
let v1 = r#"{% autoescape "none" %}v1{{name}}{% endautoescape %}"#;
let v2 = r#"{% autoescape "none" %}v2{{name}}{% endautoescape %}"#;
let (_dir, handler, shared, entry) = seed_handler(v1);
let gen_before = handler.current_generation();
fs::write(&entry, v2).expect("write v2");
let (staged, read_gen) = handler.build().await.expect("build v2");
assert_eq!(read_gen, gen_before);
handler.commit(staged);
assert_eq!(handler.current_generation(), gen_before + 1);
let set = shared.load_full();
let rendered = set
.render_entry(ctx_name("k"), ResolvedLimits::default())
.await
.expect("render v2");
assert_eq!(rendered, "v2k");
}
#[tokio::test]
async fn reload_commit_is_infallible() {
let v1 = r#"{% autoescape "none" %}v1{{name}}{% endautoescape %}"#;
let v2 = r#"{% autoescape "none" %}v2{{name}}{% endautoescape %}"#;
let (_dir, handler, shared, entry) = seed_handler(v1);
let gen_before = handler.current_generation();
fs::write(&entry, v2).expect("write v2");
let (staged, _read_gen) = handler.build().await.expect("build v2");
let returned: () = handler.commit(staged);
assert_eq!(returned, ());
assert_eq!(handler.current_generation(), gen_before + 1);
let set = shared.load_full();
let rendered = set
.render_entry(ctx_name("k"), ResolvedLimits::default())
.await
.expect("render v2");
assert_eq!(rendered, "v2k");
}
#[tokio::test]
async fn reload_handler_impls_target() {
use camel_component_api::template_reload::TemplateReloadTarget;
let v1 = r#"{% autoescape "none" %}v1{{name}}{% endautoescape %}"#;
let v2 = r#"{% autoescape "none" %}v2{{name}}{% endautoescape %}"#;
let (_dir, handler, shared, entry) = seed_handler(v1);
let gen_before = handler.current_generation();
assert_eq!(TemplateReloadTarget::route_id(&*handler), "test-reload");
assert_eq!(
TemplateReloadTarget::reload_timeout(&*handler),
std::time::Duration::from_millis(5000)
);
assert_eq!(
TemplateReloadTarget::current_generation(&*handler),
gen_before
);
fs::write(&entry, v2).expect("write v2");
let (staged, read_gen) = TemplateReloadTarget::build(&*handler)
.await
.expect("trait build v2");
assert_eq!(read_gen, gen_before);
TemplateReloadTarget::commit(&*handler, staged);
assert_eq!(handler.current_generation(), gen_before + 1);
let set = shared.load_full();
let rendered = set
.render_entry(ctx_name("k"), ResolvedLimits::default())
.await
.expect("render v2");
assert_eq!(rendered, "v2k");
}
}