clnrm_template/
builder.rs

1//! Template engine builder for comprehensive configuration
2//!
3//! Provides a fluent API for configuring template engines with all available features:
4//! - Template discovery and loading
5//! - Context configuration
6//! - Validation rules
7//! - Custom functions and filters
8//! - Caching and performance options
9//! - Output format configuration
10
11use 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
24/// Comprehensive template engine builder
25///
26/// Provides a fluent API for configuring all aspects of the template engine:
27///
28/// ```rust
29/// use clnrm_template::TemplateEngineBuilder;
30///
31/// let engine = TemplateEngineBuilder::new()
32///     .with_search_paths(vec!["./templates", "./configs"])
33///     .with_context_defaults()
34///     .with_validation_rules(vec![
35///         ValidationRule::ServiceName,
36///         ValidationRule::Semver,
37///     ])
38///     .with_custom_function("my_func", |args| Ok(Value::String("custom".to_string())))
39///     .with_cache(Duration::from_secs(3600))
40///     .with_hot_reload(true)
41///     .build()
42///     .unwrap();
43/// ```
44pub struct TemplateEngineBuilder {
45    /// Template discovery configuration
46    discovery: TemplateDiscovery,
47    /// Context configuration
48    context_builder: TemplateContextBuilder,
49    /// Validation configuration
50    validator: TemplateValidator,
51    /// Custom function registry
52    function_registry: FunctionRegistry,
53    /// TOML operations configuration
54    toml_loader: TomlLoader,
55    toml_writer: TomlWriter,
56    toml_merger: TomlMerger,
57    /// Cache configuration
58    cache_config: Option<(bool, Duration)>, // (hot_reload, ttl)
59    /// Output format
60    output_format: OutputFormat,
61    /// Debug configuration
62    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    /// Create new template engine builder
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Configure template discovery
89    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    /// Add search paths for template discovery
98    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    /// Add glob patterns for template discovery
110    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    /// Set template organization strategy
122    pub fn with_organization(mut self, organization: TemplateOrganization) -> Self {
123        self.discovery = self.discovery.with_organization(organization);
124        self
125    }
126
127    /// Configure template context
128    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    /// Use default PRD v1.0 context variables
137    pub fn with_context_defaults(mut self) -> Self {
138        self.context_builder = TemplateContextBuilder::with_defaults();
139        self
140    }
141
142    /// Add context variables
143    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    /// Load context from file
156    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    /// Configure validation
162    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    /// Add validation rules
171    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    /// Set validation format
182    pub fn with_validation_format(mut self, format: OutputFormat) -> Self {
183        // Convert renderer::OutputFormat to validation::OutputFormat
184        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    /// Add custom function
195    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    /// Add custom filter
204    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    /// Configure TOML loading
213    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    /// Configure TOML writing
222    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    /// Configure TOML merging
231    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    /// Configure caching
240    pub fn with_cache(mut self, ttl: Duration) -> Self {
241        self.cache_config = Some((true, ttl));
242        self
243    }
244
245    /// Configure caching with hot-reload
246    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    /// Disable caching
252    pub fn without_cache(mut self) -> Self {
253        self.cache_config = None;
254        self
255    }
256
257    /// Set output format
258    pub fn with_output_format(mut self, format: OutputFormat) -> Self {
259        self.output_format = format;
260        self
261    }
262
263    /// Enable debug mode
264    pub fn with_debug(mut self, debug: bool) -> Self {
265        self.debug_enabled = debug;
266        self
267    }
268
269    /// Build the template engine configuration
270    ///
271    /// Returns a configured template loader that can be used for rendering
272    pub fn build(self) -> Result<TemplateLoader> {
273        // Load templates using discovery configuration
274        let loader = self.discovery.load()?;
275
276        // Build context from configuration
277        let _context = self.context_builder.build();
278
279        // Apply validation rules to templates
280        for (name, content) in &loader.templates {
281            self.validator.validate(content, name)?;
282        }
283
284        Ok(loader)
285    }
286
287    /// Build cached renderer for performance
288    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    /// Build async cached renderer (if async feature is enabled)
297    #[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    /// Build complete template engine with all components
306    ///
307    /// Returns a struct containing all configured components for advanced usage
308    pub fn build_complete(self) -> Result<TemplateEngine> {
309        // Load templates using discovery configuration
310        let loader = self.discovery.load()?;
311
312        // Build context from configuration
313        let context = self.context_builder.build();
314
315        // Apply validation rules to templates
316        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
340/// Complete template engine with all configured components
341///
342/// Provides access to all template engine components for advanced usage scenarios
343pub struct TemplateEngine {
344    /// Template loader for template discovery and loading
345    pub loader: TemplateLoader,
346    /// Template context for variable resolution
347    pub context: TemplateContext,
348    /// Template validator for output validation
349    pub validator: TemplateValidator,
350    /// Custom function registry
351    pub function_registry: FunctionRegistry,
352    /// TOML file loader
353    pub toml_loader: TomlLoader,
354    /// TOML file writer
355    pub toml_writer: TomlWriter,
356    /// TOML merger for combining files
357    pub toml_merger: TomlMerger,
358    /// Cached renderer for performance
359    pub cache: CachedRenderer,
360    /// Default output format
361    pub output_format: OutputFormat,
362    /// Debug mode enabled
363    pub debug_enabled: bool,
364}
365
366impl TemplateEngine {
367    /// Render template by name
368    ///
369    /// # Arguments
370    /// * `name` - Template name
371    pub fn render(&mut self, name: &str) -> Result<String> {
372        self.loader.render(name, self.context.clone())
373    }
374
375    /// Render template with custom context
376    ///
377    /// # Arguments
378    /// * `name` - Template name
379    /// * `context` - Custom context for rendering
380    pub fn render_with_context(&mut self, name: &str, context: TemplateContext) -> Result<String> {
381        self.loader.render(name, context)
382    }
383
384    /// Render template to specific format
385    ///
386    /// # Arguments
387    /// * `name` - Template name
388    /// * `format` - Output format
389    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    /// Validate template output
400    ///
401    /// # Arguments
402    /// * `name` - Template name
403    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    /// Load TOML file
415    ///
416    /// # Arguments
417    /// * `path` - Path to TOML file
418    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    /// Write TOML file
423    ///
424    /// # Arguments
425    /// * `path` - Target file path
426    /// * `content` - TOML content to write
427    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    /// Get cache statistics
433    pub fn cache_stats(&self) -> crate::cache::CacheStats {
434        self.cache.cache_stats()
435    }
436
437    /// Clear cache
438    pub fn clear_cache(&self) {
439        self.cache.clear_cache();
440    }
441}
442
443/// Preset configurations for common use cases
444///
445/// Configuration for web application templates
446pub 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)) // 5 minutes for web apps
457}
458
459/// Configuration for CLI tool templates
460pub 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)) // 1 minute for CLI tools
467}
468
469/// Configuration for development workflows
470pub 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) // Hot-reload for development
478}
479
480/// Configuration for production deployments
481pub 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)) // 1 hour for production
491        .with_debug(false)
492}
493
494/// Configuration for CI/CD pipelines
495pub 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)) // 30 minutes for CI
506}
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        // Build should not fail (would need actual template files to test fully)
523        let result = builder.build();
524        assert!(result.is_err()); // Expected to fail without actual template files
525    }
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        // Test that all components are properly configured
551        assert!(engine.debug_enabled == false);
552        assert!(engine.validator.rules.len() > 0);
553        assert!(engine.context.vars.contains_key("svc"));
554    }
555}