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}