clnrm_template/
renderer.rs1use 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#[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 pub fn new() -> Result<Self> {
31 let mut tera = Tera::default();
32
33 register_functions(&mut tera, None)?;
35
36 crate::functions::extended::register_extended_functions(&mut tera);
38
39 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 pub fn with_defaults() -> Result<Self> {
57 let mut tera = Tera::default();
58
59 register_functions(&mut tera, None)?;
61
62 crate::functions::extended::register_extended_functions(&mut tera);
64
65 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 pub fn with_context(mut self, context: TemplateContext) -> Self {
80 self.context = context;
81 self
82 }
83
84 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 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 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 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 pub fn render_str(&mut self, template: &str, name: &str) -> Result<String> {
145 let tera_ctx = self.context.to_tera_context()?;
147
148 self.tera.render_str(template, &tera_ctx).map_err(|e| {
150 TemplateError::RenderError(format!("Template rendering failed in '{}': {}", name, e))
151 })
152 }
153
154 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 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 pub fn render_from_glob(&mut self, glob_pattern: &str, template_name: &str) -> Result<String> {
192 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 let tera_ctx = self.context.to_tera_context()?;
204
205 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 pub fn enable_inheritance(self) -> Result<Self> {
218 Ok(self)
220 }
221
222 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 pub fn template_names(&self) -> Vec<&str> {
233 self.tera.get_template_names().collect()
234 }
235
236 pub fn has_template(&self, name: &str) -> bool {
238 self.tera.templates.contains_key(name)
239 }
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Default)]
244pub enum OutputFormat {
245 #[default]
247 Toml,
248 Json,
250 Yaml,
252 Plain,
254}
255
256pub fn render_template(
258 template_content: &str,
259 user_vars: std::collections::HashMap<String, serde_json::Value>,
260) -> Result<String> {
261 let mut renderer = TemplateRenderer::with_defaults()?;
263
264 renderer.merge_user_vars(user_vars);
266
267 renderer.render_str(template_content, "template")
269}
270
271pub fn render_template_file(
284 template_path: &Path,
285 user_vars: std::collections::HashMap<String, serde_json::Value>,
286) -> Result<String> {
287 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_template(&template_content, user_vars)
293}
294
295pub fn is_template(content: &str) -> bool {
302 content.contains("{{") || content.contains("{%") || content.contains("{#")
303}
304
305pub fn get_cached_template_renderer() -> Result<TemplateRenderer> {
308 static INSTANCE: OnceLock<Result<TemplateRenderer>> = OnceLock::new();
309 INSTANCE.get_or_init(TemplateRenderer::new).clone()
310}