use crate::cache::CachedRenderer;
use crate::context::{TemplateContext, TemplateContextBuilder};
use crate::custom::{CustomFilter, CustomFunction, FunctionRegistry};
use crate::discovery::{TemplateDiscovery, TemplateLoader, TemplateOrganization};
use crate::error::{Result, TemplateError};
use crate::renderer::OutputFormat;
use crate::toml::{TomlLoader, TomlMerger, TomlWriter};
use crate::validation::{TemplateValidator, ValidationRule};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
pub struct TemplateEngineBuilder {
discovery: TemplateDiscovery,
context_builder: TemplateContextBuilder,
validator: TemplateValidator,
function_registry: FunctionRegistry,
toml_loader: TomlLoader,
toml_writer: TomlWriter,
toml_merger: TomlMerger,
cache_config: Option<(bool, Duration)>, output_format: OutputFormat,
debug_enabled: bool,
}
impl Default for TemplateEngineBuilder {
fn default() -> Self {
Self {
discovery: TemplateDiscovery::new(),
context_builder: TemplateContextBuilder::new(),
validator: TemplateValidator::new(),
function_registry: FunctionRegistry::new(),
toml_loader: TomlLoader::new(),
toml_writer: TomlWriter::new(),
toml_merger: TomlMerger::new(),
cache_config: None,
output_format: OutputFormat::Toml,
debug_enabled: false,
}
}
}
impl TemplateEngineBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_discovery<F>(mut self, f: F) -> Self
where
F: FnOnce(TemplateDiscovery) -> TemplateDiscovery,
{
self.discovery = f(self.discovery);
self
}
pub fn with_search_paths<I, P>(mut self, paths: I) -> Self
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
for path in paths {
self.discovery = self.discovery.with_search_path(path);
}
self
}
pub fn with_glob_patterns<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
for pattern in patterns {
self.discovery = self.discovery.with_glob_pattern(pattern.as_ref());
}
self
}
pub fn with_organization(mut self, organization: TemplateOrganization) -> Self {
self.discovery = self.discovery.with_organization(organization);
self
}
pub fn with_context<F>(mut self, f: F) -> Self
where
F: FnOnce(TemplateContextBuilder) -> TemplateContextBuilder,
{
self.context_builder = f(self.context_builder);
self
}
pub fn with_context_defaults(mut self) -> Self {
self.context_builder = TemplateContextBuilder::with_defaults();
self
}
pub fn with_variables<I, K, V>(mut self, variables: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<Value>,
{
for (key, value) in variables {
self.context_builder = self.context_builder.var(key, value);
}
self
}
pub fn with_context_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
self.context_builder = self.context_builder.load_vars_from_file(path)?;
Ok(self)
}
pub fn with_validation<F>(mut self, f: F) -> Self
where
F: FnOnce(TemplateValidator) -> TemplateValidator,
{
self.validator = f(self.validator);
self
}
pub fn with_validation_rules<I>(mut self, rules: I) -> Self
where
I: IntoIterator<Item = ValidationRule>,
{
for rule in rules {
self.validator = self.validator.with_rule(rule);
}
self
}
pub fn with_validation_format(mut self, format: OutputFormat) -> Self {
let validation_format = match format {
OutputFormat::Toml => crate::validation::OutputFormat::Toml,
OutputFormat::Json => crate::validation::OutputFormat::Json,
OutputFormat::Yaml => crate::validation::OutputFormat::Yaml,
OutputFormat::Plain => crate::validation::OutputFormat::Auto,
};
self.validator = self.validator.format(validation_format);
self
}
pub fn with_custom_function<F>(mut self, func: CustomFunction<F>) -> Self
where
F: Fn(&HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
{
self.function_registry = self.function_registry.add_function(func);
self
}
pub fn with_custom_filter<F>(mut self, filter: CustomFilter<F>) -> Self
where
F: Fn(&Value, &HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
{
self.function_registry = self.function_registry.add_filter(filter);
self
}
pub fn with_toml_loader<F>(mut self, f: F) -> Self
where
F: FnOnce(TomlLoader) -> TomlLoader,
{
self.toml_loader = f(self.toml_loader);
self
}
pub fn with_toml_writer<F>(mut self, f: F) -> Self
where
F: FnOnce(TomlWriter) -> TomlWriter,
{
self.toml_writer = f(self.toml_writer);
self
}
pub fn with_toml_merger<F>(mut self, f: F) -> Self
where
F: FnOnce(TomlMerger) -> TomlMerger,
{
self.toml_merger = f(self.toml_merger);
self
}
pub fn with_cache(mut self, ttl: Duration) -> Self {
self.cache_config = Some((true, ttl));
self
}
pub fn with_cache_and_reload(mut self, ttl: Duration, hot_reload: bool) -> Self {
self.cache_config = Some((hot_reload, ttl));
self
}
pub fn without_cache(mut self) -> Self {
self.cache_config = None;
self
}
pub fn with_output_format(mut self, format: OutputFormat) -> Self {
self.output_format = format;
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug_enabled = debug;
self
}
pub fn build(self) -> Result<TemplateLoader> {
let loader = self.discovery.load()?;
let _context = self.context_builder.build();
for (name, content) in &loader.templates {
self.validator.validate(content, name)?;
}
Ok(loader)
}
pub fn build_cached(self) -> Result<CachedRenderer> {
let context = self.context_builder.build();
let (hot_reload, _ttl) = self
.cache_config
.unwrap_or((true, Duration::from_secs(3600)));
CachedRenderer::new(context, hot_reload)
}
#[cfg(feature = "async")]
pub async fn build_async_cached(self) -> Result<crate::r#async::AsyncTemplateRenderer> {
let context = self.context_builder.build();
Ok(crate::r#async::AsyncTemplateRenderer::with_defaults()
.await?
.with_context(context))
}
pub fn build_complete(self) -> Result<TemplateEngine> {
let loader = self.discovery.load()?;
let context = self.context_builder.build();
for (name, content) in &loader.templates {
self.validator.validate(content, name)?;
}
let (hot_reload, _ttl) = self
.cache_config
.unwrap_or((true, Duration::from_secs(3600)));
let cached_renderer = CachedRenderer::new(context.clone(), hot_reload)?;
Ok(TemplateEngine {
loader,
context,
validator: self.validator,
function_registry: self.function_registry,
toml_loader: self.toml_loader,
toml_writer: self.toml_writer,
toml_merger: self.toml_merger,
cache: cached_renderer,
output_format: self.output_format,
debug_enabled: self.debug_enabled,
})
}
}
pub struct TemplateEngine {
pub loader: TemplateLoader,
pub context: TemplateContext,
pub validator: TemplateValidator,
pub function_registry: FunctionRegistry,
pub toml_loader: TomlLoader,
pub toml_writer: TomlWriter,
pub toml_merger: TomlMerger,
pub cache: CachedRenderer,
pub output_format: OutputFormat,
pub debug_enabled: bool,
}
impl TemplateEngine {
pub fn render(&mut self, name: &str) -> Result<String> {
self.loader.render(name, self.context.clone())
}
pub fn render_with_context(&mut self, name: &str, context: TemplateContext) -> Result<String> {
self.loader.render(name, context)
}
pub fn render_to_format(&mut self, name: &str, format: OutputFormat) -> Result<String> {
let rendered = self.render(name)?;
match format {
OutputFormat::Toml => Ok(rendered),
OutputFormat::Json => crate::simple::convert_to_json(&rendered),
OutputFormat::Yaml => crate::simple::convert_to_yaml(&rendered),
OutputFormat::Plain => crate::simple::strip_template_syntax(&rendered),
}
}
pub fn validate_template(&self, name: &str) -> Result<()> {
if let Some(content) = self.loader.get_template(name) {
self.validator.validate(content, name)
} else {
Err(TemplateError::ValidationError(format!(
"Template '{}' not found",
name
)))
}
}
pub fn load_toml_file<P: AsRef<Path>>(&self, path: P) -> Result<crate::toml::TomlFile> {
self.toml_loader.load_file(path)
}
pub fn write_toml_file<P: AsRef<Path>>(&self, path: P, content: &str) -> Result<()> {
self.toml_writer
.write_file(path, content, Some(&self.validator))
}
pub fn cache_stats(&self) -> crate::cache::CacheStats {
self.cache.cache_stats()
}
pub fn clear_cache(&self) {
self.cache.clear_cache();
}
}
pub fn web_app_config() -> TemplateEngineBuilder {
TemplateEngineBuilder::new()
.with_search_paths(vec!["./templates", "./configs"])
.with_glob_patterns(vec!["**/*.toml", "**/*.json"])
.with_context_defaults()
.with_validation_rules(vec![
ValidationRule::ServiceName,
ValidationRule::OtelConfig,
])
.with_output_format(OutputFormat::Json)
.with_cache(Duration::from_secs(300)) }
pub fn cli_tool_config() -> TemplateEngineBuilder {
TemplateEngineBuilder::new()
.with_search_paths(vec!["./templates"])
.with_context_defaults()
.with_validation_rules(vec![ValidationRule::ServiceName])
.with_output_format(OutputFormat::Toml)
.with_cache(Duration::from_secs(60)) }
pub fn development_config() -> TemplateEngineBuilder {
TemplateEngineBuilder::new()
.with_search_paths(vec!["./templates", "./test-templates"])
.with_glob_patterns(vec!["**/*.toml", "**/*.tera"])
.with_context_defaults()
.with_validation_rules(vec![ValidationRule::ServiceName, ValidationRule::Semver])
.with_debug(true)
.with_cache_and_reload(Duration::from_secs(30), true) }
pub fn production_config() -> TemplateEngineBuilder {
TemplateEngineBuilder::new()
.with_search_paths(vec!["./templates", "./configs"])
.with_context_defaults()
.with_validation_rules(vec![
ValidationRule::ServiceName,
ValidationRule::Semver,
ValidationRule::OtelConfig,
])
.with_cache(Duration::from_secs(3600)) .with_debug(false)
}
pub fn ci_config() -> TemplateEngineBuilder {
TemplateEngineBuilder::new()
.with_search_paths(vec!["./.github/templates", "./templates"])
.with_context_defaults()
.with_validation_rules(vec![
ValidationRule::ServiceName,
ValidationRule::Environment {
allowed: vec!["ci".to_string(), "staging".to_string()],
},
])
.with_cache(Duration::from_secs(1800)) }
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::rules;
#[test]
fn test_template_engine_builder() {
let builder = TemplateEngineBuilder::new()
.with_search_paths(vec!["./templates"])
.with_context_defaults()
.with_validation_rules(vec![rules::service_name(), rules::semver()])
.with_output_format(OutputFormat::Json)
.with_cache(Duration::from_secs(300));
let result = builder.build();
assert!(result.is_err()); }
#[test]
fn test_preset_configurations() {
let web_config = web_app_config();
assert_eq!(web_config.output_format, OutputFormat::Json);
let cli_config = cli_tool_config();
assert_eq!(cli_config.output_format, OutputFormat::Toml);
let dev_config = development_config();
assert!(dev_config.debug_enabled);
let prod_config = production_config();
assert!(!prod_config.debug_enabled);
}
#[test]
fn test_template_engine_components() {
let engine = TemplateEngineBuilder::new()
.with_context_defaults()
.with_validation_rules(vec![rules::service_name()])
.build_complete()
.unwrap();
assert!(engine.debug_enabled == false);
assert!(engine.validator.rules.len() > 0);
assert!(engine.context.vars.contains_key("svc"));
}
}