1use crate::cache::CachedRenderer;
12use crate::context::{TemplateContext, TemplateContextBuilder};
13use crate::custom::{CustomFilter, CustomFunction, FunctionRegistry};
14use crate::discovery::{TemplateDiscovery, TemplateLoader, TemplateOrganization};
15use crate::error::{Result, TemplateError};
16use crate::renderer::OutputFormat;
17use crate::toml::{TomlLoader, TomlMerger, TomlWriter};
18use crate::validation::{TemplateValidator, ValidationRule};
19use serde_json::Value;
20use std::collections::HashMap;
21use std::path::Path;
22use std::time::Duration;
23
24pub struct TemplateEngineBuilder {
45 discovery: TemplateDiscovery,
47 context_builder: TemplateContextBuilder,
49 validator: TemplateValidator,
51 function_registry: FunctionRegistry,
53 toml_loader: TomlLoader,
55 toml_writer: TomlWriter,
56 toml_merger: TomlMerger,
57 cache_config: Option<(bool, Duration)>, output_format: OutputFormat,
61 debug_enabled: bool,
63}
64
65impl Default for TemplateEngineBuilder {
66 fn default() -> Self {
67 Self {
68 discovery: TemplateDiscovery::new(),
69 context_builder: TemplateContextBuilder::new(),
70 validator: TemplateValidator::new(),
71 function_registry: FunctionRegistry::new(),
72 toml_loader: TomlLoader::new(),
73 toml_writer: TomlWriter::new(),
74 toml_merger: TomlMerger::new(),
75 cache_config: None,
76 output_format: OutputFormat::Toml,
77 debug_enabled: false,
78 }
79 }
80}
81
82impl TemplateEngineBuilder {
83 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn with_discovery<F>(mut self, f: F) -> Self
90 where
91 F: FnOnce(TemplateDiscovery) -> TemplateDiscovery,
92 {
93 self.discovery = f(self.discovery);
94 self
95 }
96
97 pub fn with_search_paths<I, P>(mut self, paths: I) -> Self
99 where
100 I: IntoIterator<Item = P>,
101 P: AsRef<Path>,
102 {
103 for path in paths {
104 self.discovery = self.discovery.with_search_path(path);
105 }
106 self
107 }
108
109 pub fn with_glob_patterns<I, S>(mut self, patterns: I) -> Self
111 where
112 I: IntoIterator<Item = S>,
113 S: AsRef<str>,
114 {
115 for pattern in patterns {
116 self.discovery = self.discovery.with_glob_pattern(pattern.as_ref());
117 }
118 self
119 }
120
121 pub fn with_organization(mut self, organization: TemplateOrganization) -> Self {
123 self.discovery = self.discovery.with_organization(organization);
124 self
125 }
126
127 pub fn with_context<F>(mut self, f: F) -> Self
129 where
130 F: FnOnce(TemplateContextBuilder) -> TemplateContextBuilder,
131 {
132 self.context_builder = f(self.context_builder);
133 self
134 }
135
136 pub fn with_context_defaults(mut self) -> Self {
138 self.context_builder = TemplateContextBuilder::with_defaults();
139 self
140 }
141
142 pub fn with_variables<I, K, V>(mut self, variables: I) -> Self
144 where
145 I: IntoIterator<Item = (K, V)>,
146 K: Into<String>,
147 V: Into<Value>,
148 {
149 for (key, value) in variables {
150 self.context_builder = self.context_builder.var(key, value);
151 }
152 self
153 }
154
155 pub fn with_context_from_file<P: AsRef<Path>>(mut self, path: P) -> Result<Self> {
157 self.context_builder = self.context_builder.load_vars_from_file(path)?;
158 Ok(self)
159 }
160
161 pub fn with_validation<F>(mut self, f: F) -> Self
163 where
164 F: FnOnce(TemplateValidator) -> TemplateValidator,
165 {
166 self.validator = f(self.validator);
167 self
168 }
169
170 pub fn with_validation_rules<I>(mut self, rules: I) -> Self
172 where
173 I: IntoIterator<Item = ValidationRule>,
174 {
175 for rule in rules {
176 self.validator = self.validator.with_rule(rule);
177 }
178 self
179 }
180
181 pub fn with_validation_format(mut self, format: OutputFormat) -> Self {
183 let validation_format = match format {
185 OutputFormat::Toml => crate::validation::OutputFormat::Toml,
186 OutputFormat::Json => crate::validation::OutputFormat::Json,
187 OutputFormat::Yaml => crate::validation::OutputFormat::Yaml,
188 OutputFormat::Plain => crate::validation::OutputFormat::Auto,
189 };
190 self.validator = self.validator.format(validation_format);
191 self
192 }
193
194 pub fn with_custom_function<F>(mut self, func: CustomFunction<F>) -> Self
196 where
197 F: Fn(&HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
198 {
199 self.function_registry = self.function_registry.add_function(func);
200 self
201 }
202
203 pub fn with_custom_filter<F>(mut self, filter: CustomFilter<F>) -> Self
205 where
206 F: Fn(&Value, &HashMap<String, Value>) -> Result<Value> + Send + Sync + 'static,
207 {
208 self.function_registry = self.function_registry.add_filter(filter);
209 self
210 }
211
212 pub fn with_toml_loader<F>(mut self, f: F) -> Self
214 where
215 F: FnOnce(TomlLoader) -> TomlLoader,
216 {
217 self.toml_loader = f(self.toml_loader);
218 self
219 }
220
221 pub fn with_toml_writer<F>(mut self, f: F) -> Self
223 where
224 F: FnOnce(TomlWriter) -> TomlWriter,
225 {
226 self.toml_writer = f(self.toml_writer);
227 self
228 }
229
230 pub fn with_toml_merger<F>(mut self, f: F) -> Self
232 where
233 F: FnOnce(TomlMerger) -> TomlMerger,
234 {
235 self.toml_merger = f(self.toml_merger);
236 self
237 }
238
239 pub fn with_cache(mut self, ttl: Duration) -> Self {
241 self.cache_config = Some((true, ttl));
242 self
243 }
244
245 pub fn with_cache_and_reload(mut self, ttl: Duration, hot_reload: bool) -> Self {
247 self.cache_config = Some((hot_reload, ttl));
248 self
249 }
250
251 pub fn without_cache(mut self) -> Self {
253 self.cache_config = None;
254 self
255 }
256
257 pub fn with_output_format(mut self, format: OutputFormat) -> Self {
259 self.output_format = format;
260 self
261 }
262
263 pub fn with_debug(mut self, debug: bool) -> Self {
265 self.debug_enabled = debug;
266 self
267 }
268
269 pub fn build(self) -> Result<TemplateLoader> {
273 let loader = self.discovery.load()?;
275
276 let _context = self.context_builder.build();
278
279 for (name, content) in &loader.templates {
281 self.validator.validate(content, name)?;
282 }
283
284 Ok(loader)
285 }
286
287 pub fn build_cached(self) -> Result<CachedRenderer> {
289 let context = self.context_builder.build();
290 let (hot_reload, _ttl) = self
291 .cache_config
292 .unwrap_or((true, Duration::from_secs(3600)));
293 CachedRenderer::new(context, hot_reload)
294 }
295
296 #[cfg(feature = "async")]
298 pub async fn build_async_cached(self) -> Result<crate::r#async::AsyncTemplateRenderer> {
299 let context = self.context_builder.build();
300 Ok(crate::r#async::AsyncTemplateRenderer::with_defaults()
301 .await?
302 .with_context(context))
303 }
304
305 pub fn build_complete(self) -> Result<TemplateEngine> {
309 let loader = self.discovery.load()?;
311
312 let context = self.context_builder.build();
314
315 for (name, content) in &loader.templates {
317 self.validator.validate(content, name)?;
318 }
319
320 let (hot_reload, _ttl) = self
321 .cache_config
322 .unwrap_or((true, Duration::from_secs(3600)));
323 let cached_renderer = CachedRenderer::new(context.clone(), hot_reload)?;
324
325 Ok(TemplateEngine {
326 loader,
327 context,
328 validator: self.validator,
329 function_registry: self.function_registry,
330 toml_loader: self.toml_loader,
331 toml_writer: self.toml_writer,
332 toml_merger: self.toml_merger,
333 cache: cached_renderer,
334 output_format: self.output_format,
335 debug_enabled: self.debug_enabled,
336 })
337 }
338}
339
340pub struct TemplateEngine {
344 pub loader: TemplateLoader,
346 pub context: TemplateContext,
348 pub validator: TemplateValidator,
350 pub function_registry: FunctionRegistry,
352 pub toml_loader: TomlLoader,
354 pub toml_writer: TomlWriter,
356 pub toml_merger: TomlMerger,
358 pub cache: CachedRenderer,
360 pub output_format: OutputFormat,
362 pub debug_enabled: bool,
364}
365
366impl TemplateEngine {
367 pub fn render(&mut self, name: &str) -> Result<String> {
372 self.loader.render(name, self.context.clone())
373 }
374
375 pub fn render_with_context(&mut self, name: &str, context: TemplateContext) -> Result<String> {
381 self.loader.render(name, context)
382 }
383
384 pub fn render_to_format(&mut self, name: &str, format: OutputFormat) -> Result<String> {
390 let rendered = self.render(name)?;
391 match format {
392 OutputFormat::Toml => Ok(rendered),
393 OutputFormat::Json => crate::simple::convert_to_json(&rendered),
394 OutputFormat::Yaml => crate::simple::convert_to_yaml(&rendered),
395 OutputFormat::Plain => crate::simple::strip_template_syntax(&rendered),
396 }
397 }
398
399 pub fn validate_template(&self, name: &str) -> Result<()> {
404 if let Some(content) = self.loader.get_template(name) {
405 self.validator.validate(content, name)
406 } else {
407 Err(TemplateError::ValidationError(format!(
408 "Template '{}' not found",
409 name
410 )))
411 }
412 }
413
414 pub fn load_toml_file<P: AsRef<Path>>(&self, path: P) -> Result<crate::toml::TomlFile> {
419 self.toml_loader.load_file(path)
420 }
421
422 pub fn write_toml_file<P: AsRef<Path>>(&self, path: P, content: &str) -> Result<()> {
428 self.toml_writer
429 .write_file(path, content, Some(&self.validator))
430 }
431
432 pub fn cache_stats(&self) -> crate::cache::CacheStats {
434 self.cache.cache_stats()
435 }
436
437 pub fn clear_cache(&self) {
439 self.cache.clear_cache();
440 }
441}
442
443pub fn web_app_config() -> TemplateEngineBuilder {
447 TemplateEngineBuilder::new()
448 .with_search_paths(vec!["./templates", "./configs"])
449 .with_glob_patterns(vec!["**/*.toml", "**/*.json"])
450 .with_context_defaults()
451 .with_validation_rules(vec![
452 ValidationRule::ServiceName,
453 ValidationRule::OtelConfig,
454 ])
455 .with_output_format(OutputFormat::Json)
456 .with_cache(Duration::from_secs(300)) }
458
459pub fn cli_tool_config() -> TemplateEngineBuilder {
461 TemplateEngineBuilder::new()
462 .with_search_paths(vec!["./templates"])
463 .with_context_defaults()
464 .with_validation_rules(vec![ValidationRule::ServiceName])
465 .with_output_format(OutputFormat::Toml)
466 .with_cache(Duration::from_secs(60)) }
468
469pub fn development_config() -> TemplateEngineBuilder {
471 TemplateEngineBuilder::new()
472 .with_search_paths(vec!["./templates", "./test-templates"])
473 .with_glob_patterns(vec!["**/*.toml", "**/*.tera"])
474 .with_context_defaults()
475 .with_validation_rules(vec![ValidationRule::ServiceName, ValidationRule::Semver])
476 .with_debug(true)
477 .with_cache_and_reload(Duration::from_secs(30), true) }
479
480pub fn production_config() -> TemplateEngineBuilder {
482 TemplateEngineBuilder::new()
483 .with_search_paths(vec!["./templates", "./configs"])
484 .with_context_defaults()
485 .with_validation_rules(vec![
486 ValidationRule::ServiceName,
487 ValidationRule::Semver,
488 ValidationRule::OtelConfig,
489 ])
490 .with_cache(Duration::from_secs(3600)) .with_debug(false)
492}
493
494pub fn ci_config() -> TemplateEngineBuilder {
496 TemplateEngineBuilder::new()
497 .with_search_paths(vec!["./.github/templates", "./templates"])
498 .with_context_defaults()
499 .with_validation_rules(vec![
500 ValidationRule::ServiceName,
501 ValidationRule::Environment {
502 allowed: vec!["ci".to_string(), "staging".to_string()],
503 },
504 ])
505 .with_cache(Duration::from_secs(1800)) }
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::validation::rules;
512
513 #[test]
514 fn test_template_engine_builder() {
515 let builder = TemplateEngineBuilder::new()
516 .with_search_paths(vec!["./templates"])
517 .with_context_defaults()
518 .with_validation_rules(vec![rules::service_name(), rules::semver()])
519 .with_output_format(OutputFormat::Json)
520 .with_cache(Duration::from_secs(300));
521
522 let result = builder.build();
524 assert!(result.is_err()); }
526
527 #[test]
528 fn test_preset_configurations() {
529 let web_config = web_app_config();
530 assert_eq!(web_config.output_format, OutputFormat::Json);
531
532 let cli_config = cli_tool_config();
533 assert_eq!(cli_config.output_format, OutputFormat::Toml);
534
535 let dev_config = development_config();
536 assert!(dev_config.debug_enabled);
537
538 let prod_config = production_config();
539 assert!(!prod_config.debug_enabled);
540 }
541
542 #[test]
543 fn test_template_engine_components() {
544 let engine = TemplateEngineBuilder::new()
545 .with_context_defaults()
546 .with_validation_rules(vec![rules::service_name()])
547 .build_complete()
548 .unwrap();
549
550 assert!(engine.debug_enabled == false);
552 assert!(engine.validator.rules.len() > 0);
553 assert!(engine.context.vars.contains_key("svc"));
554 }
555}