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#[derive(Debug)]
31pub struct TemplateEngine<'a> {
32 handlebars: Handlebars<'a>,
33}
34
35impl<'a> TemplateEngine<'a> {
36 /// Creates a new template engine with registered templates.
37 ///
38 /// Registers all built-in progressive loading templates.
39 ///
40 /// # Errors
41 ///
42 /// Returns error if template registration fails (should not happen
43 /// with valid built-in templates).
44 ///
45 /// # Examples
46 ///
47 /// ```
48 /// use mcp_execution_codegen::template_engine::TemplateEngine;
49 ///
50 /// let engine = TemplateEngine::new().unwrap();
51 /// ```
52 pub fn new() -> Result<Self> {
53 let mut handlebars = Handlebars::new();
54
55 // Strict mode: fail on missing variables
56 handlebars.set_strict_mode(true);
57
58 // Register progressive loading templates
59 Self::register_progressive_templates(&mut handlebars)?;
60
61 Ok(Self { handlebars })
62 }
63
64 /// Registers progressive loading templates.
65 ///
66 /// Registers templates for progressive loading pattern where each tool
67 /// is a separate file.
68 fn register_progressive_templates(handlebars: &mut Handlebars<'a>) -> Result<()> {
69 // Tool template: generates a single tool function (progressive loading)
70 handlebars
71 .register_template_string(
72 "progressive/tool",
73 include_str!("../templates/progressive/tool.ts.hbs"),
74 )
75 .map_err(|e| Error::SerializationError {
76 message: format!("Failed to register progressive tool template: {e}"),
77 source: None,
78 })?;
79
80 // Index template: generates index.ts with re-exports (progressive loading)
81 handlebars
82 .register_template_string(
83 "progressive/index",
84 include_str!("../templates/progressive/index.ts.hbs"),
85 )
86 .map_err(|e| Error::SerializationError {
87 message: format!("Failed to register progressive index template: {e}"),
88 source: None,
89 })?;
90
91 // Runtime bridge template: generates runtime helper for MCP calls
92 handlebars
93 .register_template_string(
94 "progressive/runtime-bridge",
95 include_str!("../templates/progressive/runtime-bridge.ts.hbs"),
96 )
97 .map_err(|e| Error::SerializationError {
98 message: format!("Failed to register progressive runtime-bridge template: {e}"),
99 source: None,
100 })?;
101
102 Ok(())
103 }
104
105 /// Renders a template with the given context.
106 ///
107 /// # Errors
108 ///
109 /// Returns error if:
110 /// - Template name is not registered
111 /// - Context cannot be serialized
112 /// - Template rendering fails
113 ///
114 /// # Examples
115 ///
116 /// ```no_run
117 /// use mcp_execution_codegen::template_engine::TemplateEngine;
118 /// use serde_json::json;
119 ///
120 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
121 /// let engine = TemplateEngine::new()?;
122 /// let context = json!({"name": "test", "description": "A test tool"});
123 /// let result = engine.render("progressive/tool", &context)?;
124 /// # Ok(())
125 /// # }
126 /// ```
127 pub fn render<T: Serialize>(&self, template_name: &str, context: &T) -> Result<String> {
128 self.handlebars
129 .render(template_name, context)
130 .map_err(|e| Error::SerializationError {
131 message: format!("Template rendering failed: {e}"),
132 source: None,
133 })
134 }
135
136 /// Registers a custom template.
137 ///
138 /// Allows registering additional templates at runtime.
139 ///
140 /// # Errors
141 ///
142 /// Returns error if template string is invalid.
143 ///
144 /// # Examples
145 ///
146 /// ```
147 /// use mcp_execution_codegen::template_engine::TemplateEngine;
148 ///
149 /// let mut engine = TemplateEngine::new().unwrap();
150 /// engine.register_template_string(
151 /// "custom",
152 /// "// Custom template: {{name}}"
153 /// ).unwrap();
154 /// ```
155 pub fn register_template_string(&mut self, name: &str, template: &str) -> Result<()> {
156 self.handlebars
157 .register_template_string(name, template)
158 .map_err(|e| Error::SerializationError {
159 message: format!("Failed to register template '{name}': {e}"),
160 source: None,
161 })
162 }
163}
164
165impl<'a> Default for TemplateEngine<'a> {
166 fn default() -> Self {
167 Self::new().expect("Failed to create default TemplateEngine")
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use serde_json::json;
175
176 #[test]
177 fn test_template_engine_creation() {
178 let engine = TemplateEngine::new();
179 assert!(engine.is_ok());
180 }
181
182 #[test]
183 fn test_render_progressive_templates() {
184 let engine = TemplateEngine::new().unwrap();
185
186 // Test progressive/tool template
187 let tool_context = json!({
188 "typescript_name": "testTool",
189 "description": "Test tool",
190 "server_id": "test",
191 "name": "test_tool",
192 "properties": [],
193 "has_required_properties": false,
194 "input_schema": {}
195 });
196
197 let result = engine.render("progressive/tool", &tool_context);
198 if let Err(e) = &result {
199 eprintln!("Error rendering template: {}", e);
200 }
201 assert!(result.is_ok(), "Failed to render: {:?}", result.err());
202 assert!(result.unwrap().contains("testTool"));
203 }
204
205 #[test]
206 fn test_custom_template_registration() {
207 let mut engine = TemplateEngine::new().unwrap();
208
209 engine
210 .register_template_string("test", "Hello {{name}}")
211 .unwrap();
212
213 let context = json!({"name": "World"});
214 let result = engine.render("test", &context).unwrap();
215 assert_eq!(result, "Hello World");
216 }
217
218 #[test]
219 fn test_render_nonexistent_template() {
220 let engine = TemplateEngine::new().unwrap();
221 let context = json!({"name": "test"});
222 let result = engine.render("nonexistent", &context);
223 assert!(result.is_err());
224 }
225
226 #[test]
227 fn test_default_trait() {
228 let _engine = TemplateEngine::default();
229 }
230}