1use 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
16pub 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
42pub 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
71pub 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
96pub fn render_with_context(template: &str, _context: &TemplateContext) -> Result<String> {
113 let mut renderer = TemplateRenderer::new()?;
114 renderer.render_str(template, "template")
115}
116
117pub 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#[derive(Debug, Clone, Copy)]
157pub enum OutputFormat {
158 Toml,
160 Json,
162 Yaml,
164 Plain,
166}
167
168pub 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
178pub 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
188pub fn strip_template_syntax(content: &str) -> Result<String> {
190 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
230pub 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 pub fn new() -> Self {
266 Self::default()
267 }
268
269 pub fn template<S: Into<String>>(mut self, template: S) -> Self {
271 self.template = Some(template.into());
272 self
273 }
274
275 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 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 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 pub fn format(mut self, format: OutputFormat) -> Self {
304 self.format = format;
305 self
306 }
307
308 pub fn context(mut self, context: TemplateContext) -> Self {
310 self.context = Some(context);
311 self
312 }
313
314 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 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
355pub mod quick {
357 use super::*;
358
359 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 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 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 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#[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#[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 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}