clnrm_template/
async.rs

1//! Async template rendering support
2//!
3//! Provides async versions of template rendering functions for use in async applications:
4//! - Async template rendering
5//! - Async file operations
6//! - Async template discovery
7//! - Async caching and hot-reload
8
9use crate::context::TemplateContext;
10use crate::error::{Result, TemplateError};
11use crate::renderer::{OutputFormat, TemplateRenderer};
12use serde_json::Value;
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16/// Async template renderer for async applications
17///
18/// Provides async versions of all template rendering operations
19pub struct AsyncTemplateRenderer {
20    /// Base template renderer
21    renderer: TemplateRenderer,
22}
23
24impl AsyncTemplateRenderer {
25    /// Create new async template renderer
26    pub async fn new() -> Result<Self> {
27        let renderer = TemplateRenderer::new()?;
28        Ok(Self { renderer })
29    }
30
31    /// Create renderer with default context
32    pub async fn with_defaults() -> Result<Self> {
33        let renderer = TemplateRenderer::with_defaults()?;
34        Ok(Self { renderer })
35    }
36
37    /// Set template context
38    pub fn with_context(mut self, context: TemplateContext) -> Self {
39        self.renderer = self.renderer.with_context(context);
40        self
41    }
42
43    /// Render template string asynchronously
44    ///
45    /// # Arguments
46    /// * `template` - Template content
47    /// * `name` - Template name for error reporting
48    pub async fn render_str(&mut self, template: &str, name: &str) -> Result<String> {
49        // Template rendering is CPU-bound, so we run it in a blocking task
50        // Clone strings to move into spawn_blocking
51        let template = template.to_string();
52        let name = name.to_string();
53        let mut renderer = self.renderer.clone();
54        tokio::task::spawn_blocking(move || renderer.render_str(&template, &name))
55            .await
56            .map_err(|e| TemplateError::InternalError(format!("Async rendering failed: {}", e)))?
57    }
58
59    /// Render template to specific format
60    ///
61    /// # Arguments
62    /// * `template` - Template content
63    /// * `name` - Template name
64    /// * `format` - Output format
65    pub async fn render_to_format(
66        &mut self,
67        template: &str,
68        name: &str,
69        format: OutputFormat,
70    ) -> Result<String> {
71        let rendered = self.render_str(template, name).await?;
72
73        match format {
74            OutputFormat::Toml => Ok(rendered),
75            OutputFormat::Json => crate::simple::convert_to_json(&rendered),
76            OutputFormat::Yaml => crate::simple::convert_to_yaml(&rendered),
77            OutputFormat::Plain => crate::simple::strip_template_syntax(&rendered),
78        }
79    }
80
81    /// Render template file asynchronously
82    ///
83    /// # Arguments
84    /// * `path` - Path to template file
85    pub async fn render_file<P: AsRef<Path>>(&mut self, path: P) -> Result<String> {
86        let path = path.as_ref().to_path_buf();
87        tokio::task::spawn_blocking(move || {
88            let mut renderer = TemplateRenderer::new()?;
89            renderer.render_file(&path)
90        })
91        .await
92        .map_err(|e| TemplateError::InternalError(format!("Async file rendering failed: {}", e)))?
93    }
94
95    /// Merge user variables into context
96    pub fn merge_user_vars(&mut self, user_vars: HashMap<String, Value>) {
97        self.renderer.merge_user_vars(user_vars);
98    }
99
100    /// Access the underlying renderer
101    pub fn renderer(&self) -> &TemplateRenderer {
102        &self.renderer
103    }
104
105    /// Access the underlying renderer mutably
106    pub fn renderer_mut(&mut self) -> &mut TemplateRenderer {
107        &mut self.renderer
108    }
109}
110
111/// Async convenience functions for simple template rendering
112///
113/// Render template string asynchronously
114///
115/// # Arguments
116/// * `template` - Template content
117/// * `vars` - Variables as key-value pairs
118pub async fn async_render(template: &str, vars: HashMap<&str, &str>) -> Result<String> {
119    let mut json_vars = HashMap::new();
120    for (key, value) in vars {
121        json_vars.insert(key.to_string(), Value::String(value.to_string()));
122    }
123
124    let mut renderer = AsyncTemplateRenderer::new().await?;
125    renderer.merge_user_vars(json_vars);
126    renderer.render_str(template, "async_template").await
127}
128
129/// Render template file asynchronously
130///
131/// # Arguments
132/// * `path` - Path to template file
133/// * `vars` - Variables as key-value pairs
134pub async fn async_render_file<P: AsRef<Path>>(
135    path: P,
136    vars: HashMap<&str, &str>,
137) -> Result<String> {
138    let mut json_vars = HashMap::new();
139    for (key, value) in vars {
140        json_vars.insert(key.to_string(), Value::String(value.to_string()));
141    }
142
143    let mut renderer = AsyncTemplateRenderer::new().await?;
144    renderer.merge_user_vars(json_vars);
145    renderer.render_file(path).await
146}
147
148/// Render template with JSON variables asynchronously
149pub async fn async_render_with_json(template: &str, vars: HashMap<&str, Value>) -> Result<String> {
150    let mut json_vars = HashMap::new();
151    for (key, value) in vars {
152        json_vars.insert(key.to_string(), value);
153    }
154
155    let mut renderer = AsyncTemplateRenderer::new().await?;
156    renderer.merge_user_vars(json_vars);
157    renderer.render_str(template, "async_template").await
158}
159
160/// Async template builder for fluent configuration
161///
162/// Provides async versions of the template builder API
163pub struct AsyncTemplateBuilder {
164    template: Option<String>,
165    variables: HashMap<String, Value>,
166    format: OutputFormat,
167    context: Option<TemplateContext>,
168}
169
170impl Default for AsyncTemplateBuilder {
171    fn default() -> Self {
172        Self {
173            template: None,
174            variables: HashMap::new(),
175            format: OutputFormat::Toml,
176            context: None,
177        }
178    }
179}
180
181impl AsyncTemplateBuilder {
182    /// Create new async template builder
183    pub fn new() -> Self {
184        Self::default()
185    }
186
187    /// Set template content
188    pub fn template<S: Into<String>>(mut self, template: S) -> Self {
189        self.template = Some(template.into());
190        self
191    }
192
193    /// Add string variable
194    pub fn variable<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
195        self.variables
196            .insert(key.into(), Value::String(value.into()));
197        self
198    }
199
200    /// Add JSON variable
201    pub fn json_variable<K: Into<String>>(mut self, key: K, value: Value) -> Self {
202        self.variables.insert(key.into(), value);
203        self
204    }
205
206    /// Set output format
207    pub fn format(mut self, format: OutputFormat) -> Self {
208        self.format = format;
209        self
210    }
211
212    /// Set custom context
213    pub fn context(mut self, context: TemplateContext) -> Self {
214        self.context = Some(context);
215        self
216    }
217
218    /// Render template asynchronously
219    pub async fn render(self) -> Result<String> {
220        let template = self
221            .template
222            .ok_or_else(|| TemplateError::ValidationError("No template provided".to_string()))?;
223
224        if let Some(context) = self.context {
225            let mut renderer = AsyncTemplateRenderer::new().await?.with_context(context);
226            let result = renderer.render_str(&template, "async_template").await?;
227
228            match self.format {
229                OutputFormat::Toml => Ok(result),
230                OutputFormat::Json => crate::simple::convert_to_json(&result),
231                OutputFormat::Yaml => crate::simple::convert_to_yaml(&result),
232                OutputFormat::Plain => crate::simple::strip_template_syntax(&result),
233            }
234        } else {
235            let mut json_vars = HashMap::new();
236            for (key, value) in self.variables {
237                json_vars.insert(key, value);
238            }
239
240            let mut renderer = AsyncTemplateRenderer::new().await?;
241            renderer.merge_user_vars(json_vars);
242            let result = renderer.render_str(&template, "async_template").await?;
243
244            match self.format {
245                OutputFormat::Toml => Ok(result),
246                OutputFormat::Json => crate::simple::convert_to_json(&result),
247                OutputFormat::Yaml => crate::simple::convert_to_yaml(&result),
248                OutputFormat::Plain => crate::simple::strip_template_syntax(&result),
249            }
250        }
251    }
252}
253
254/// Async TOML file operations for async applications
255pub mod async_toml {
256    use super::*;
257    use crate::toml::{TomlFile, TomlLoader, TomlWriter};
258    use std::collections::HashMap;
259
260    /// Load TOML file asynchronously
261    ///
262    /// # Arguments
263    /// * `path` - Path to TOML file
264    pub async fn load_toml_file<P: AsRef<Path>>(path: P) -> Result<TomlFile> {
265        let path = path.as_ref().to_path_buf();
266        tokio::task::spawn_blocking(move || {
267            let loader = TomlLoader::new();
268            loader.load_file(path)
269        })
270        .await
271        .map_err(|e| TemplateError::InternalError(format!("Async TOML loading failed: {}", e)))?
272    }
273
274    /// Load all TOML files from directory asynchronously
275    ///
276    /// # Arguments
277    /// * `search_paths` - Directories to search
278    pub async fn load_all_toml_files(
279        search_paths: Vec<&Path>,
280    ) -> Result<HashMap<PathBuf, TomlFile>> {
281        let paths: Vec<PathBuf> = search_paths.iter().map(|p| p.to_path_buf()).collect();
282        tokio::task::spawn_blocking(move || {
283            let loader = TomlLoader::new().with_search_paths(paths);
284            loader.load_all()
285        })
286        .await
287        .map_err(|e| {
288            TemplateError::InternalError(format!("Async TOML directory loading failed: {}", e))
289        })?
290    }
291
292    /// Write TOML file asynchronously
293    ///
294    /// # Arguments
295    /// * `path` - Target file path
296    /// * `content` - TOML content to write
297    /// * `validator` - Optional validator
298    pub async fn write_toml_file<P: AsRef<Path>>(
299        path: P,
300        content: &str,
301        validator: Option<&crate::validation::TemplateValidator>,
302    ) -> Result<()> {
303        let path = path.as_ref().to_path_buf();
304        let content = content.to_string();
305        let validator = validator.cloned();
306
307        tokio::task::spawn_blocking(move || {
308            let writer = TomlWriter::new();
309            writer.write_file(path, &content, validator.as_ref())
310        })
311        .await
312        .map_err(|e| TemplateError::InternalError(format!("Async TOML writing failed: {}", e)))?
313    }
314}
315
316/// Async template discovery for large codebases
317pub mod async_discovery {
318    use super::*;
319    use crate::discovery::{TemplateDiscovery, TemplateLoader};
320
321    /// Discover templates asynchronously
322    ///
323    /// # Arguments
324    /// * `search_paths` - Directories to search
325    /// * `patterns` - Glob patterns to match
326    pub async fn discover_templates(
327        search_paths: Vec<&Path>,
328        patterns: Vec<&str>,
329    ) -> Result<TemplateLoader> {
330        let paths: Vec<PathBuf> = search_paths.iter().map(|p| p.to_path_buf()).collect();
331        let patterns: Vec<String> = patterns.iter().map(|s| s.to_string()).collect();
332
333        tokio::task::spawn_blocking(move || {
334            let mut discovery = TemplateDiscovery::new();
335            for path in paths {
336                discovery = discovery.with_search_path(path);
337            }
338            for pattern in patterns {
339                discovery = discovery.with_glob_pattern(&pattern);
340            }
341            discovery.load()
342        })
343        .await
344        .map_err(|e| {
345            TemplateError::InternalError(format!("Async template discovery failed: {}", e))
346        })?
347    }
348}
349
350/// Async template validation for large templates
351pub mod async_validation {
352    use super::*;
353
354    /// Validate template output asynchronously
355    ///
356    /// # Arguments
357    /// * `output` - Rendered template content
358    /// * `template_name` - Template name for error reporting
359    /// * `validator` - Template validator
360    pub async fn validate_async(
361        output: &str,
362        template_name: &str,
363        validator: &crate::validation::TemplateValidator,
364    ) -> Result<()> {
365        let output = output.to_string();
366        let template_name = template_name.to_string();
367        let validator = validator.clone();
368        tokio::task::spawn_blocking(move || validator.validate(&output, &template_name))
369            .await
370            .map_err(|e| TemplateError::InternalError(format!("Async validation failed: {}", e)))?
371    }
372}
373
374/// Async template caching for high-performance applications
375pub mod async_cache {
376    use super::*;
377    use crate::cache::CachedRenderer;
378
379    /// Create async cached renderer
380    ///
381    /// # Arguments
382    /// * `context` - Template context
383    /// * `hot_reload` - Enable hot-reload
384    pub async fn create_async_cached_renderer(
385        context: TemplateContext,
386        hot_reload: bool,
387    ) -> Result<CachedRenderer> {
388        tokio::task::spawn_blocking(move || CachedRenderer::new(context, hot_reload))
389            .await
390            .map_err(|e| {
391                TemplateError::InternalError(format!(
392                    "Async cached renderer creation failed: {}",
393                    e
394                ))
395            })?
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[tokio::test]
404    async fn test_async_render() {
405        let result = async_render(
406            "Hello {{ name }}!",
407            [("name", "World")].iter().cloned().collect(),
408        )
409        .await
410        .unwrap();
411        assert_eq!(result, "Hello World!");
412    }
413
414    #[tokio::test]
415    async fn test_async_template_builder() {
416        let result = AsyncTemplateBuilder::new()
417            .template("Service: {{ service }}")
418            .variable("service", "my-service")
419            .render()
420            .await
421            .unwrap();
422
423        assert_eq!(result, "Service: my-service");
424    }
425
426    #[tokio::test]
427    async fn test_async_toml_loading() {
428        use std::fs;
429        use tempfile::tempdir;
430
431        let temp_dir = tempdir().unwrap();
432        let toml_file = temp_dir.path().join("test.toml");
433
434        let content = r#"
435[service]
436name = "test-service"
437        "#;
438
439        fs::write(&toml_file, content).unwrap();
440
441        let file = async_toml::load_toml_file(&toml_file).await.unwrap();
442        assert_eq!(file.path, toml_file);
443        assert!(file.parsed.get("service").is_some());
444    }
445}