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#[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            "name_literal": "test_tool",
193            "server_id_literal": "test",
194            "properties": [],
195            "has_required_properties": false,
196            "input_schema": {}
197        });
198
199        let result = engine.render("progressive/tool", &tool_context);
200        if let Err(e) = &result {
201            eprintln!("Error rendering template: {}", e);
202        }
203        assert!(result.is_ok(), "Failed to render: {:?}", result.err());
204        assert!(result.unwrap().contains("testTool"));
205    }
206
207    #[test]
208    fn test_custom_template_registration() {
209        let mut engine = TemplateEngine::new().unwrap();
210
211        engine
212            .register_template_string("test", "Hello {{name}}")
213            .unwrap();
214
215        let context = json!({"name": "World"});
216        let result = engine.render("test", &context).unwrap();
217        assert_eq!(result, "Hello World");
218    }
219
220    #[test]
221    fn test_render_nonexistent_template() {
222        let engine = TemplateEngine::new().unwrap();
223        let context = json!({"name": "test"});
224        let result = engine.render("nonexistent", &context);
225        assert!(result.is_err());
226    }
227
228    #[test]
229    fn test_default_trait() {
230        let _engine = TemplateEngine::default();
231    }
232}