Skip to main content

mcp_execution_codegen/
template_engine.rs

1//! Template engine for code generation using Handlebars.
2//!
3//! Provides a wrapper around Handlebars with pre-registered templates
4//! for TypeScript code generation with progressive loading.
5//!
6//! # Examples
7//!
8//! ```
9//! use mcp_execution_codegen::template_engine::TemplateEngine;
10//! use serde_json::json;
11//!
12//! let engine = TemplateEngine::new().unwrap();
13//! let context = json!({"name": "test"});
14//! // let result = engine.render("progressive/tool", &context).unwrap();
15//! ```
16
17use handlebars::Handlebars;
18use mcp_execution_core::{Error, Result};
19use serde::Serialize;
20
21/// Template engine for code generation.
22///
23/// Wraps Handlebars and provides pre-registered templates for
24/// generating TypeScript code from MCP tool schemas using progressive loading.
25///
26/// # Thread Safety
27///
28/// This type is `Send` and `Sync`, allowing it to be used across
29/// thread boundaries safely.
30///
31/// # Examples
32///
33/// ```
34/// use mcp_execution_codegen::template_engine::TemplateEngine;
35///
36/// let engine = TemplateEngine::new().unwrap();
37/// // engine can now render templates
38/// ```
39#[derive(Debug)]
40pub struct TemplateEngine<'a> {
41    handlebars: Handlebars<'a>,
42}
43
44impl<'a> TemplateEngine<'a> {
45    /// Creates a new template engine with registered templates.
46    ///
47    /// Registers all built-in progressive loading templates.
48    ///
49    /// # Errors
50    ///
51    /// Returns error if template registration fails (should not happen
52    /// with valid built-in templates).
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use mcp_execution_codegen::template_engine::TemplateEngine;
58    ///
59    /// let engine = TemplateEngine::new().unwrap();
60    /// ```
61    pub fn new() -> Result<Self> {
62        let mut handlebars = Handlebars::new();
63
64        // Strict mode: fail on missing variables
65        handlebars.set_strict_mode(true);
66
67        // Handlebars HTML-escapes `{{var}}` by default (`&`, `<`, `>`, `"`, `'`),
68        // which corrupts the TypeScript/JSDoc source this engine generates. Injection
69        // safety is instead enforced upstream by `sanitize_jsdoc` and
70        // `sanitize_ts_string_literal` (see `progressive/generator.rs`), which run
71        // before rendering and strip the sequences that actually matter (`*/`,
72        // unescaped quotes, newlines).
73        handlebars.register_escape_fn(handlebars::no_escape);
74
75        // Register progressive loading templates
76        Self::register_progressive_templates(&mut handlebars)?;
77
78        Ok(Self { handlebars })
79    }
80
81    /// Registers progressive loading templates.
82    ///
83    /// Registers templates for progressive loading pattern where each tool
84    /// is a separate file.
85    fn register_progressive_templates(handlebars: &mut Handlebars<'a>) -> Result<()> {
86        // Tool template: generates a single tool function (progressive loading)
87        handlebars
88            .register_template_string(
89                "progressive/tool",
90                include_str!("../templates/progressive/tool.ts.hbs"),
91            )
92            .map_err(|e| Error::SerializationError {
93                message: format!("Failed to register progressive tool template: {e}"),
94                source: None,
95            })?;
96
97        // Index template: generates index.ts with re-exports (progressive loading)
98        handlebars
99            .register_template_string(
100                "progressive/index",
101                include_str!("../templates/progressive/index.ts.hbs"),
102            )
103            .map_err(|e| Error::SerializationError {
104                message: format!("Failed to register progressive index template: {e}"),
105                source: None,
106            })?;
107
108        // Runtime bridge template: generates runtime helper for MCP calls
109        handlebars
110            .register_template_string(
111                "progressive/runtime-bridge",
112                include_str!("../templates/progressive/runtime-bridge.ts.hbs"),
113            )
114            .map_err(|e| Error::SerializationError {
115                message: format!("Failed to register progressive runtime-bridge template: {e}"),
116                source: None,
117            })?;
118
119        Ok(())
120    }
121
122    /// Renders a template with the given context.
123    ///
124    /// # Errors
125    ///
126    /// Returns error if:
127    /// - Template name is not registered
128    /// - Context cannot be serialized
129    /// - Template rendering fails
130    ///
131    /// # Examples
132    ///
133    /// ```no_run
134    /// use mcp_execution_codegen::template_engine::TemplateEngine;
135    /// use serde_json::json;
136    ///
137    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
138    /// let engine = TemplateEngine::new()?;
139    /// let context = json!({"name": "test", "description": "A test tool"});
140    /// let result = engine.render("progressive/tool", &context)?;
141    /// # Ok(())
142    /// # }
143    /// ```
144    pub fn render<T: Serialize>(&self, template_name: &str, context: &T) -> Result<String> {
145        self.handlebars
146            .render(template_name, context)
147            .map_err(|e| Error::SerializationError {
148                message: format!("Template rendering failed: {e}"),
149                source: None,
150            })
151    }
152
153    /// Registers a custom template.
154    ///
155    /// Allows registering additional templates at runtime.
156    ///
157    /// # Errors
158    ///
159    /// Returns error if template string is invalid.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use mcp_execution_codegen::template_engine::TemplateEngine;
165    ///
166    /// let mut engine = TemplateEngine::new().unwrap();
167    /// engine.register_template_string(
168    ///     "custom",
169    ///     "// Custom template: {{name}}"
170    /// ).unwrap();
171    /// ```
172    pub fn register_template_string(&mut self, name: &str, template: &str) -> Result<()> {
173        self.handlebars
174            .register_template_string(name, template)
175            .map_err(|e| Error::SerializationError {
176                message: format!("Failed to register template '{name}': {e}"),
177                source: None,
178            })
179    }
180}
181
182impl Default for TemplateEngine<'_> {
183    fn default() -> Self {
184        Self::new().expect("Failed to create default TemplateEngine")
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use serde_json::json;
192
193    #[test]
194    fn test_template_engine_creation() {
195        let engine = TemplateEngine::new();
196        assert!(engine.is_ok());
197    }
198
199    #[test]
200    fn test_render_progressive_templates() {
201        let engine = TemplateEngine::new().unwrap();
202
203        // Test progressive/tool template
204        let tool_context = json!({
205            "typescript_name": "testTool",
206            "description": "Test tool",
207            "server_id": "test",
208            "name": "test_tool",
209            "name_literal": "test_tool",
210            "server_id_literal": "test",
211            "properties": [],
212            "has_required_properties": false,
213            "input_schema": {}
214        });
215
216        let result = engine.render("progressive/tool", &tool_context);
217        if let Err(e) = &result {
218            eprintln!("Error rendering template: {e}");
219        }
220        assert!(result.is_ok(), "Failed to render: {:?}", result.err());
221        assert!(result.unwrap().contains("testTool"));
222    }
223
224    #[test]
225    fn test_custom_template_registration() {
226        let mut engine = TemplateEngine::new().unwrap();
227
228        engine
229            .register_template_string("test", "Hello {{name}}")
230            .unwrap();
231
232        let context = json!({"name": "World"});
233        let result = engine.render("test", &context).unwrap();
234        assert_eq!(result, "Hello World");
235    }
236
237    #[test]
238    fn test_render_nonexistent_template() {
239        let engine = TemplateEngine::new().unwrap();
240        let context = json!({"name": "test"});
241        let result = engine.render("nonexistent", &context);
242        assert!(result.is_err());
243    }
244
245    #[test]
246    fn test_default_trait() {
247        let _engine = TemplateEngine::default();
248    }
249
250    #[test]
251    fn test_render_does_not_html_escape() {
252        // Handlebars HTML-escapes `{{var}}` by default; this project's templates
253        // interpolate into TypeScript/JSDoc, not HTML, so that must be disabled.
254        let mut engine = TemplateEngine::new().unwrap();
255        engine
256            .register_template_string("test-no-escape", "{{value}}")
257            .unwrap();
258
259        let context = json!({"value": "a && b < c > d \"e\" 'f'"});
260        let result = engine.render("test-no-escape", &context).unwrap();
261
262        assert_eq!(result, "a && b < c > d \"e\" 'f'");
263    }
264}