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}