Skip to main content

ralph_workflow/prompts/
template_engine.rs

1//! Template engine for rendering prompt templates.
2//!
3//! This module provides a template variable replacement system for prompt templates
4//! with support for variables, partials, comments, conditionals, loops, and defaults.
5//!
6//! ## Syntax
7//!
8//! - **Variables**: `{{VARIABLE}}` or `{{ VARIABLE }}` - replaced with values
9//! - **Default values**: `{{VARIABLE|default="value"}}` - uses value if VARIABLE is missing
10//! - **Conditionals**: `{% if VARIABLE %}...{% endif %}` - include content if VARIABLE is truthy
11//! - **Negation**: `{% if !VARIABLE %}...{% endif %}` - include content if VARIABLE is falsy
12//! - **Loops**: `{% for item in ITEMS %}...{% endfor %}` - iterate over comma-separated values
13//! - **Partials**: `{{> partial_name}}` or `{{> partial/path}}` - includes another template
14//! - **Comments**: `{# comment #}` - stripped from output, useful for documentation
15//!
16//! ## Partials System
17//!
18//! Partials allow sharing common template sections across multiple templates.
19//! When a partial is referenced, it's looked up from the provided partials map
20//! and recursively rendered with the same variables.
21//!
22//! Example partial include:
23//! ```text
24//! {{> shared/_critical_header}}
25//! ```
26//!
27//! The partials system:
28//! - Detects and prevents circular references
29//! - Provides clear error messages for missing partials
30//! - Supports hierarchical naming (dot notation or path-style)
31
32use std::collections::HashMap;
33
34/// Error type for template operations.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum TemplateError {
37    /// Required variable not provided.
38    MissingVariable(String),
39    /// Referenced partial not found in partials map.
40    PartialNotFound(String),
41    /// Circular reference detected in partial includes.
42    CircularReference(Vec<String>),
43}
44
45impl std::fmt::Display for TemplateError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::MissingVariable(name) => write!(f, "Missing required variable: {{{{ {name} }}}}"),
49            Self::PartialNotFound(name) => {
50                write!(f, "Partial not found: '{{> {name}}}'")
51            }
52            Self::CircularReference(chain) => {
53                write!(f, "Circular reference detected in partials: ")?;
54                let mut sep = "";
55                for partial in chain {
56                    write!(f, "{sep}{{{{> {partial}}}}}")?;
57                    sep = " -> ";
58                }
59                Ok(())
60            }
61        }
62    }
63}
64
65impl std::error::Error for TemplateError {}
66
67/// A simple template engine for prompt templates.
68///
69/// Templates use `{{VARIABLE}}` syntax for placeholders and `{{> partial}}` for
70/// including shared templates. Variables are replaced with the provided values.
71/// Comments using `{# comment #}` syntax are stripped.
72///
73/// # Example
74///
75/// ```ignore
76/// let partials = HashMap::from([("header", "Common Header\n")]);
77/// let template = Template::new("{{> header}}\nReview this diff:\n{{DIFF}}");
78/// let variables = HashMap::from([("DIFF", "+ new line")]);
79/// let rendered = template.render_with_partials(&variables, &partials)?;
80/// ```
81#[derive(Debug, Clone)]
82pub struct Template {
83    /// The template content with comments and partials processed.
84    content: String,
85}
86
87impl Template {
88    /// Create a template from a string.
89    ///
90    /// Comments (`{# ... #}`) are stripped during creation.
91    /// All features are enabled by default: variables, conditionals, loops, and defaults.
92    pub fn new(content: &str) -> Self {
93        // Strip comments first
94        let content = Self::strip_comments(content);
95        Self { content }
96    }
97}
98
99include!("template_engine/parser.rs");
100include!("template_engine/renderer.rs");
101include!("template_engine/tests.rs");