clnrm_template/
simple.rs

1//! Simple API for basic template rendering use cases
2//!
3//! Provides dead-simple functions for common template rendering scenarios:
4//! - `render(template, vars)` - Basic string rendering
5//! - `render_file(path, vars)` - File-based rendering
6//! - `render_with_context(template, context)` - Advanced rendering with context
7//! - `TemplateBuilder` - Fluent API for complex configurations
8
9use crate::context::TemplateContext;
10use crate::error::{Result, TemplateError};
11use crate::renderer::{render_template, TemplateRenderer};
12use serde_json::Value;
13use std::collections::HashMap;
14use std::path::Path;
15
16/// Render template string with variables (simplest API)
17///
18/// # Arguments
19/// * `template` - Template string with {{ variables }}
20/// * `vars` - Variables as key-value pairs
21///
22/// # Example
23/// ```rust
24/// use clnrm_template::render;
25/// use std::collections::HashMap;
26///
27/// let mut vars = HashMap::new();
28/// vars.insert("name", "World");
29/// vars.insert("count", "42");
30///
31/// let result = render("Hello {{ name }}! Count: {{ count }}", vars).unwrap();
32/// assert_eq!(result, "Hello World! Count: 42");
33/// ```
34pub fn render(template: &str, vars: HashMap<&str, &str>) -> Result<String> {
35    let mut json_vars = HashMap::new();
36    for (key, value) in vars {
37        json_vars.insert(key.to_string(), Value::String(value.to_string()));
38    }
39    render_template(template, json_vars)
40}
41
42/// Render template string with JSON values
43///
44/// # Arguments
45/// * `template` - Template string with {{ variables }}
46/// * `vars` - Variables as JSON values
47///
48/// # Example
49/// ```rust
50/// use clnrm_template::render_with_json;
51/// use std::collections::HashMap;
52/// use serde_json::Value;
53///
54/// let mut vars = HashMap::new();
55/// vars.insert("items", Value::Array(vec![
56///     Value::String("apple".to_string()),
57///     Value::String("banana".to_string())
58/// ]));
59/// vars.insert("enabled", Value::Bool(true));
60///
61/// let result = render_with_json("Items: {{ items | join(', ') }}, Enabled: {{ enabled }}", vars).unwrap();
62/// ```
63pub fn render_with_json(template: &str, vars: HashMap<&str, Value>) -> Result<String> {
64    let mut json_vars = HashMap::new();
65    for (key, value) in vars {
66        json_vars.insert(key.to_string(), value);
67    }
68    render_template(template, json_vars)
69}
70
71/// Render template file with variables
72///
73/// # Arguments
74/// * `path` - Path to template file
75/// * `vars` - Variables as key-value pairs
76///
77/// # Example
78/// ```rust
79/// use clnrm_template::render_file;
80/// use std::collections::HashMap;
81///
82/// let mut vars = HashMap::new();
83/// vars.insert("service", "my-service");
84/// vars.insert("port", "8080");
85///
86/// let result = render_file("templates/config.toml", vars).unwrap();
87/// ```
88pub fn render_file<P: AsRef<Path>>(path: P, vars: HashMap<&str, &str>) -> Result<String> {
89    let mut json_vars = HashMap::new();
90    for (key, value) in vars {
91        json_vars.insert(key.to_string(), Value::String(value.to_string()));
92    }
93    crate::renderer::render_template_file(path.as_ref(), json_vars)
94}
95
96/// Render template with pre-built context
97///
98/// # Arguments
99/// * `template` - Template string
100/// * `context` - Pre-configured template context
101///
102/// # Example
103/// ```rust
104/// use clnrm_template::{render_with_context, TemplateContext};
105///
106/// let context = TemplateContext::with_defaults()
107///     .var("service", "my-service")
108///     .var("environment", "production");
109///
110/// let result = render_with_context("Service: {{ service }}, Env: {{ environment }}", &context).unwrap();
111/// ```
112pub fn render_with_context(template: &str, _context: &TemplateContext) -> Result<String> {
113    let mut renderer = TemplateRenderer::new()?;
114    renderer.render_str(template, "template")
115}
116
117/// Render template to specific output format
118///
119/// # Arguments
120/// * `template` - Template string
121/// * `vars` - Variables as key-value pairs
122/// * `format` - Desired output format
123///
124/// # Example
125/// ```rust
126/// use clnrm_template::{render_to_format, OutputFormat};
127/// use std::collections::HashMap;
128///
129/// let mut vars = HashMap::new();
130/// vars.insert("name", "test");
131/// vars.insert("value", "123");
132///
133/// let result = render_to_format("Name: {{ name }}, Value: {{ value }}", vars, OutputFormat::Json).unwrap();
134/// ```
135pub fn render_to_format(
136    template: &str,
137    vars: HashMap<&str, &str>,
138    format: OutputFormat,
139) -> Result<String> {
140    let mut json_vars = HashMap::new();
141    for (key, value) in vars {
142        json_vars.insert(key.to_string(), Value::String(value.to_string()));
143    }
144
145    let rendered = render_template(template, json_vars)?;
146
147    match format {
148        OutputFormat::Toml => Ok(rendered),
149        OutputFormat::Json => convert_to_json(&rendered),
150        OutputFormat::Yaml => convert_to_yaml(&rendered),
151        OutputFormat::Plain => strip_template_syntax(&rendered),
152    }
153}
154
155/// Output format for template rendering
156#[derive(Debug, Clone, Copy)]
157pub enum OutputFormat {
158    /// TOML format (default)
159    Toml,
160    /// JSON format
161    Json,
162    /// YAML format
163    Yaml,
164    /// Plain text (remove template syntax)
165    Plain,
166}
167
168/// Convert TOML to JSON format
169pub fn convert_to_json(toml_content: &str) -> Result<String> {
170    let parsed: Value = toml::from_str(toml_content).map_err(|e| {
171        TemplateError::ValidationError(format!("Failed to parse TOML for JSON conversion: {}", e))
172    })?;
173
174    serde_json::to_string_pretty(&parsed)
175        .map_err(|e| TemplateError::ValidationError(format!("Failed to serialize to JSON: {}", e)))
176}
177
178/// Convert TOML to YAML format
179pub fn convert_to_yaml(toml_content: &str) -> Result<String> {
180    let parsed: Value = toml::from_str(toml_content).map_err(|e| {
181        TemplateError::ValidationError(format!("Failed to parse TOML for YAML conversion: {}", e))
182    })?;
183
184    serde_yaml::to_string(&parsed)
185        .map_err(|e| TemplateError::ValidationError(format!("Failed to serialize to YAML: {}", e)))
186}
187
188/// Strip template syntax to get plain text
189pub fn strip_template_syntax(content: &str) -> Result<String> {
190    // Simple implementation - remove {{ }} and {% %} blocks
191    let mut result = String::new();
192    let mut in_braces = false;
193    let mut brace_depth = 0;
194
195    for ch in content.chars() {
196        match ch {
197            '{' => {
198                if let Some(next) = content.chars().nth(result.len() + 1) {
199                    if next == '{' || next == '%' {
200                        in_braces = true;
201                        brace_depth = 1;
202                        continue;
203                    }
204                }
205            }
206            '}' => {
207                if in_braces {
208                    if let Some(prev) = content.chars().nth(result.len() - 1) {
209                        if prev == '}' || prev == '%' {
210                            brace_depth -= 1;
211                            if brace_depth == 0 {
212                                in_braces = false;
213                            }
214                            continue;
215                        }
216                    }
217                }
218            }
219            _ => {
220                if !in_braces {
221                    result.push(ch);
222                }
223            }
224        }
225    }
226
227    Ok(result)
228}
229
230/// Template builder for fluent configuration
231///
232/// Provides a simple, chainable API for template rendering:
233///
234/// ```rust
235/// use clnrm_template::TemplateBuilder;
236///
237/// let result = TemplateBuilder::new()
238///     .template("Hello {{ name }}!")
239///     .variable("name", "World")
240///     .variable("count", "42")
241///     .format(OutputFormat::Plain)
242///     .render()
243///     .unwrap();
244/// ```
245pub struct TemplateBuilder {
246    template: Option<String>,
247    variables: HashMap<String, Value>,
248    format: OutputFormat,
249    context: Option<TemplateContext>,
250}
251
252impl Default for TemplateBuilder {
253    fn default() -> Self {
254        Self {
255            template: None,
256            variables: HashMap::new(),
257            format: OutputFormat::Toml,
258            context: None,
259        }
260    }
261}
262
263impl TemplateBuilder {
264    /// Create new template builder
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    /// Set template content
270    pub fn template<S: Into<String>>(mut self, template: S) -> Self {
271        self.template = Some(template.into());
272        self
273    }
274
275    /// Add string variable
276    pub fn variable<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
277        self.variables
278            .insert(key.into(), Value::String(value.into()));
279        self
280    }
281
282    /// Add JSON variable
283    pub fn json_variable<K: Into<String>>(mut self, key: K, value: Value) -> Self {
284        self.variables.insert(key.into(), value);
285        self
286    }
287
288    /// Add multiple variables at once
289    pub fn variables<I, K, V>(mut self, vars: I) -> Self
290    where
291        I: IntoIterator<Item = (K, V)>,
292        K: Into<String>,
293        V: Into<String>,
294    {
295        for (key, value) in vars {
296            self.variables
297                .insert(key.into(), Value::String(value.into()));
298        }
299        self
300    }
301
302    /// Set output format
303    pub fn format(mut self, format: OutputFormat) -> Self {
304        self.format = format;
305        self
306    }
307
308    /// Set custom context
309    pub fn context(mut self, context: TemplateContext) -> Self {
310        self.context = Some(context);
311        self
312    }
313
314    /// Render template
315    pub fn render(self) -> Result<String> {
316        let template = self
317            .template
318            .ok_or_else(|| TemplateError::ValidationError("No template provided".to_string()))?;
319
320        if let Some(context) = self.context {
321            render_with_context(&template, &context)
322        } else {
323            let rendered = render_template(&template, self.variables)?;
324            match self.format {
325                OutputFormat::Toml => Ok(rendered),
326                OutputFormat::Json => convert_to_json(&rendered),
327                OutputFormat::Yaml => convert_to_yaml(&rendered),
328                OutputFormat::Plain => strip_template_syntax(&rendered),
329            }
330        }
331    }
332
333    /// Render template file
334    pub fn render_file<P: AsRef<Path>>(self, path: P) -> Result<String> {
335        let _template = self
336            .template
337            .ok_or_else(|| TemplateError::ValidationError("No template provided".to_string()))?;
338
339        let mut json_vars = HashMap::new();
340        for (key, value) in self.variables {
341            json_vars.insert(key, value);
342        }
343
344        let result = crate::renderer::render_template_file(path.as_ref(), json_vars)?;
345
346        match self.format {
347            OutputFormat::Toml => Ok(result),
348            OutputFormat::Json => convert_to_json(&result),
349            OutputFormat::Yaml => convert_to_yaml(&result),
350            OutputFormat::Plain => strip_template_syntax(&result),
351        }
352    }
353}
354
355/// Quick template rendering functions for common patterns
356pub mod quick {
357    use super::*;
358
359    /// Render a simple greeting template
360    pub fn greeting(name: &str) -> String {
361        render(
362            "Hello {{ name }}!",
363            [("name", name)].iter().cloned().collect(),
364        )
365        .unwrap_or_default()
366    }
367
368    /// Render a configuration template
369    pub fn config(service: &str, port: u16) -> String {
370        render(
371            "[service]\nname = \"{{ service }}\"\nport = {{ port }}",
372            [("service", service), ("port", &port.to_string())]
373                .iter()
374                .cloned()
375                .collect(),
376        )
377        .unwrap_or_default()
378    }
379
380    /// Render a JSON template
381    pub fn json_template(name: &str, value: &str) -> String {
382        render_to_format(
383            "{\"name\": \"{{ name }}\", \"value\": \"{{ value }}\"}",
384            [("name", name), ("value", value)].iter().cloned().collect(),
385            OutputFormat::Json,
386        )
387        .unwrap_or_default()
388    }
389
390    /// Render a YAML template
391    pub fn yaml_template(title: &str, items: Vec<&str>) -> String {
392        let _items_str = items.join("\", \"");
393        render_to_format(
394            "title: {{ title }}\nitems:\n  - \"{{ items | join('\",\n  - \"') }}\"",
395            [("title", title)].iter().cloned().collect(),
396            OutputFormat::Yaml,
397        )
398        .unwrap_or_default()
399    }
400}
401
402/// Template macros for compile-time template rendering
403///
404/// These macros allow embedding template rendering at compile time:
405///
406/// ```rust
407/// const CONFIG: &str = template!("service = \"{{ name }}\"", name = "my-service");
408/// ```
409#[macro_export]
410macro_rules! template {
411    ($template:expr) => {
412        $template
413    };
414
415    ($template:expr, $($key:ident = $value:expr),* $(,)?) => {{
416        let mut vars = std::collections::HashMap::new();
417        $(
418            vars.insert(stringify!($key).to_string(), serde_json::Value::String($value.to_string()));
419        )*
420        $crate::render_template($template, vars).unwrap_or_else(|_| $template.to_string())
421    }};
422}
423
424/// Template literals for embedded templates
425///
426/// ```rust
427/// use clnrm_template::template_literal;
428///
429/// const TEMPLATE: &str = template_literal!("Hello {{ name }}!");
430/// ```
431#[macro_export]
432macro_rules! template_literal {
433    ($template:expr) => {
434        $template
435    };
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_simple_render() {
444        let result = render(
445            "Hello {{ name }}!",
446            [("name", "World")].iter().cloned().collect(),
447        )
448        .unwrap();
449        assert_eq!(result, "Hello World!");
450    }
451
452    #[test]
453    fn test_render_with_json() {
454        let mut vars = HashMap::new();
455        vars.insert(
456            "items",
457            Value::Array(vec![
458                Value::String("apple".to_string()),
459                Value::String("banana".to_string()),
460            ]),
461        );
462
463        let result = render_with_json("Items: {{ items | length }}", vars).unwrap();
464        // Note: This would need the length filter to be implemented
465        assert!(result.contains("Items:"));
466    }
467
468    #[test]
469    fn test_template_builder() {
470        let result = TemplateBuilder::new()
471            .template("Service: {{ service }}, Port: {{ port }}")
472            .variable("service", "my-service")
473            .variable("port", "8080")
474            .render()
475            .unwrap();
476
477        assert_eq!(result, "Service: my-service, Port: 8080");
478    }
479
480    #[test]
481    fn test_output_formats() {
482        let toml_result = render_to_format(
483            "name = \"{{ name }}\"",
484            [("name", "test")].iter().cloned().collect(),
485            OutputFormat::Toml,
486        )
487        .unwrap();
488        assert_eq!(toml_result, "name = \"test\"");
489
490        let json_result = render_to_format(
491            "{\"name\": \"{{ name }}\"}",
492            [("name", "test")].iter().cloned().collect(),
493            OutputFormat::Json,
494        )
495        .unwrap();
496        assert!(json_result.contains("\"name\""));
497        assert!(json_result.contains("\"test\""));
498    }
499
500    #[test]
501    fn test_quick_templates() {
502        let greeting = quick::greeting("Alice");
503        assert_eq!(greeting, "Hello Alice!");
504
505        let config = quick::config("web-server", 3000);
506        assert!(config.contains("web-server"));
507        assert!(config.contains("3000"));
508    }
509}