clnrm_template/
renderer.rs

1//! Tera template rendering for .clnrm.toml files
2//!
3//! This module provides template rendering capabilities for test configuration files,
4//! enabling dynamic test generation with custom functions.
5
6use crate::context::TemplateContext;
7use crate::error::{Result, TemplateError};
8use crate::functions::{register_functions, TimestampProvider};
9use std::path::Path;
10use std::sync::OnceLock;
11use tera::Tera;
12
13/// Template renderer with Tera engine
14///
15/// Provides template rendering with custom functions for:
16/// - Environment variable access
17/// - Deterministic timestamps
18/// - SHA-256 hashing
19/// - TOML encoding
20/// - Macro library for common TOML patterns
21#[derive(Clone)]
22pub struct TemplateRenderer {
23    pub(crate) tera: Tera,
24    context: TemplateContext,
25    determinism: Option<std::sync::Arc<dyn TimestampProvider + Send + Sync>>,
26}
27
28impl TemplateRenderer {
29    /// Create new template renderer with custom functions and macro library
30    pub fn new() -> Result<Self> {
31        let mut tera = Tera::default();
32
33        // Register custom functions (no determinism engine)
34        register_functions(&mut tera, None)?;
35
36        // Register extended functions (UUID, string transforms, time helpers, OTEL)
37        crate::functions::extended::register_extended_functions(&mut tera);
38
39        // Add macro library template
40        tera.add_raw_template("_macros.toml.tera", crate::MACRO_LIBRARY)
41            .map_err(|e| {
42                TemplateError::RenderError(format!("Failed to load macro library: {}", e))
43            })?;
44
45        Ok(Self {
46            tera,
47            context: TemplateContext::new(),
48            determinism: None,
49        })
50    }
51
52    /// Create renderer with default PRD v1.0 variable resolution
53    ///
54    /// Initializes context with standard variables resolved via precedence:
55    /// template vars → ENV → defaults
56    pub fn with_defaults() -> Result<Self> {
57        let mut tera = Tera::default();
58
59        // Register custom functions (no determinism engine)
60        register_functions(&mut tera, None)?;
61
62        // Register extended functions (UUID, string transforms, time helpers, OTEL)
63        crate::functions::extended::register_extended_functions(&mut tera);
64
65        // Add macro library template
66        tera.add_raw_template("_macros.toml.tera", crate::MACRO_LIBRARY)
67            .map_err(|e| {
68                TemplateError::RenderError(format!("Failed to load macro library: {}", e))
69            })?;
70
71        Ok(Self {
72            tera,
73            context: TemplateContext::with_defaults(),
74            determinism: None,
75        })
76    }
77
78    /// Set template context variables
79    pub fn with_context(mut self, context: TemplateContext) -> Self {
80        self.context = context;
81        self
82    }
83
84    /// Set determinism engine for reproducible template rendering
85    ///
86    /// When configured, this freezes `now_rfc3339()` function and provides
87    /// seeded random generation for fake data functions.
88    ///
89    /// # Arguments
90    /// * `engine` - DeterminismEngine with optional seed and freeze_clock
91    ///
92    /// # Returns
93    /// * Self with determinism enabled
94    ///
95    /// # Example
96    /// ```no_run
97    /// use clnrm_core::template::TemplateRenderer;
98    /// use clnrm_core::determinism::{DeterminismEngine, DeterminismConfig};
99    ///
100    /// let config = DeterminismConfig {
101    ///     seed: Some(42),
102    ///     freeze_clock: Some("2025-01-01T00:00:00Z".to_string()),
103    /// };
104    /// let engine = DeterminismEngine::new(config).unwrap();
105    /// let renderer = TemplateRenderer::new()
106    ///     .unwrap()
107    ///     .with_determinism(engine);
108    /// ```
109    pub fn with_determinism(
110        mut self,
111        determinism: std::sync::Arc<dyn TimestampProvider + Send + Sync>,
112    ) -> Self {
113        self.determinism = Some(determinism);
114        self
115    }
116
117    /// Merge user-provided variables into context (respects precedence)
118    ///
119    /// User variables take highest priority in the precedence chain
120    pub fn merge_user_vars(
121        &mut self,
122        user_vars: std::collections::HashMap<String, serde_json::Value>,
123    ) {
124        self.context.merge_user_vars(user_vars);
125    }
126
127    /// Render template file to TOML string
128    pub fn render_file(&mut self, path: &Path) -> Result<String> {
129        let template_str = std::fs::read_to_string(path)
130            .map_err(|e| TemplateError::IoError(format!("Failed to read template: {}", e)))?;
131
132        // Convert path to string with proper error handling
133        let path_str = path.to_str().ok_or_else(|| {
134            TemplateError::ValidationError(format!(
135                "Template path contains invalid UTF-8 characters: {}",
136                path.display()
137            ))
138        })?;
139
140        self.render_str(&template_str, path_str)
141    }
142
143    /// Render template string to TOML
144    pub fn render_str(&mut self, template: &str, name: &str) -> Result<String> {
145        // Build Tera context
146        let tera_ctx = self.context.to_tera_context()?;
147
148        // Render template
149        self.tera.render_str(template, &tera_ctx).map_err(|e| {
150            TemplateError::RenderError(format!("Template rendering failed in '{}': {}", name, e))
151        })
152    }
153
154    /// Render template to specific output format
155    ///
156    /// # Arguments
157    /// * `template` - Template content
158    /// * `name` - Template name for error reporting
159    /// * `format` - Desired output format
160    pub fn render_to_format(
161        &mut self,
162        template: &str,
163        name: &str,
164        format: OutputFormat,
165    ) -> Result<String> {
166        let rendered = self.render_str(template, name)?;
167
168        match format {
169            OutputFormat::Toml => Ok(rendered),
170            OutputFormat::Json => crate::simple::convert_to_json(&rendered),
171            OutputFormat::Yaml => crate::simple::convert_to_yaml(&rendered),
172            OutputFormat::Plain => crate::simple::strip_template_syntax(&rendered),
173        }
174    }
175
176    /// Render a template string with macro imports (for testing)
177    /// This is a helper method that handles the add_raw_template + render pattern
178    pub fn render_template_string(&mut self, template: &str, name: &str) -> Result<String> {
179        self.tera.add_raw_template(name, template).map_err(|e| {
180            TemplateError::RenderError(format!("Failed to add template '{}': {}", name, e))
181        })?;
182
183        self.tera.render(name, &tera::Context::new()).map_err(|e| {
184            TemplateError::RenderError(format!("Failed to render template '{}': {}", name, e))
185        })
186    }
187
188    /// Render template from glob pattern
189    ///
190    /// Useful for rendering multiple templates with shared context
191    pub fn render_from_glob(&mut self, glob_pattern: &str, template_name: &str) -> Result<String> {
192        // Add templates matching glob pattern
193        self.tera
194            .add_template_file(glob_pattern, Some(template_name))
195            .map_err(|e| {
196                TemplateError::RenderError(format!(
197                    "Failed to add templates from glob '{}': {}",
198                    glob_pattern, e
199                ))
200            })?;
201
202        // Build Tera context
203        let tera_ctx = self.context.to_tera_context()?;
204
205        // Render specific template
206        self.tera.render(template_name, &tera_ctx).map_err(|e| {
207            TemplateError::RenderError(format!(
208                "Template rendering failed for '{}': {}",
209                template_name, e
210            ))
211        })
212    }
213
214    /// Add template inheritance support
215    ///
216    /// Enables `{% extends %}` and `{% block %}` functionality
217    pub fn enable_inheritance(self) -> Result<Self> {
218        // Tera supports inheritance by default, but we can add custom functions
219        Ok(self)
220    }
221
222    /// Add template to the renderer
223    ///
224    /// Useful for dynamic template loading and composition
225    pub fn add_template(&mut self, name: &str, content: &str) -> Result<()> {
226        self.tera.add_raw_template(name, content).map_err(|e| {
227            TemplateError::RenderError(format!("Failed to add template '{}': {}", name, e))
228        })
229    }
230
231    /// Get available template names
232    pub fn template_names(&self) -> Vec<&str> {
233        self.tera.get_template_names().collect()
234    }
235
236    /// Check if template exists
237    pub fn has_template(&self, name: &str) -> bool {
238        self.tera.templates.contains_key(name)
239    }
240}
241
242/// Output format for template rendering
243#[derive(Debug, Clone, Copy, PartialEq, Default)]
244pub enum OutputFormat {
245    /// TOML format (default for Cleanroom)
246    #[default]
247    Toml,
248    /// JSON format
249    Json,
250    /// YAML format
251    Yaml,
252    /// Plain text (remove template syntax)
253    Plain,
254}
255
256/// Convenience functions for simple template rendering
257pub fn render_template(
258    template_content: &str,
259    user_vars: std::collections::HashMap<String, serde_json::Value>,
260) -> Result<String> {
261    // Create renderer with defaults
262    let mut renderer = TemplateRenderer::with_defaults()?;
263
264    // Merge user variables (highest precedence)
265    renderer.merge_user_vars(user_vars);
266
267    // Render template
268    renderer.render_str(template_content, "template")
269}
270
271/// Render template file with user variables and PRD v1.0 defaults
272///
273/// File-based variant of `render_template`
274///
275/// # Arguments
276///
277/// * `template_path` - Path to template file
278/// * `user_vars` - User-provided variables (highest precedence)
279///
280/// # Returns
281///
282/// Rendered TOML string ready for parsing
283pub fn render_template_file(
284    template_path: &Path,
285    user_vars: std::collections::HashMap<String, serde_json::Value>,
286) -> Result<String> {
287    // Read template file
288    let template_content = std::fs::read_to_string(template_path)
289        .map_err(|e| TemplateError::IoError(format!("Failed to read template file: {}", e)))?;
290
291    // Render with user vars
292    render_template(&template_content, user_vars)
293}
294
295/// Check if file content should be treated as a template
296///
297/// Detects Tera template syntax:
298/// - `{{ variable }}` - variable substitution
299/// - `{% for x in list %}` - control structures
300/// - `{# comment #}` - comments
301pub fn is_template(content: &str) -> bool {
302    content.contains("{{") || content.contains("{%") || content.contains("{#")
303}
304
305/// Get a cached template renderer instance
306/// This avoids recompiling Tera templates on every use for better performance
307pub fn get_cached_template_renderer() -> Result<TemplateRenderer> {
308    static INSTANCE: OnceLock<Result<TemplateRenderer>> = OnceLock::new();
309    INSTANCE.get_or_init(TemplateRenderer::new).clone()
310}