Skip to main content

mcp_execution_codegen/common/
types.rs

1//! Types for code generation.
2//!
3//! Defines the data structures used during code generation from MCP
4//! tool schemas to executable TypeScript or Rust code.
5//!
6//! # Examples
7//!
8//! ```
9//! use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
10//!
11//! let file = GeneratedFile {
12//!     path: "tools/sendMessage.ts".to_string(),
13//!     content: "export function sendMessage() {}".to_string(),
14//! };
15//!
16//! let code = GeneratedCode {
17//!     files: vec![file],
18//! };
19//!
20//! assert_eq!(code.files.len(), 1);
21//! ```
22
23use mcp_execution_core::{Error, Result};
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26
27/// Result of code generation containing all generated files.
28///
29/// This is the main output type returned by the code generator.
30/// Contains a list of files that should be written to disk.
31///
32/// # Examples
33///
34/// ```
35/// use mcp_execution_codegen::GeneratedCode;
36///
37/// let code = GeneratedCode {
38///     files: vec![],
39/// };
40///
41/// assert_eq!(code.file_count(), 0);
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct GeneratedCode {
45    /// List of generated files with paths and contents
46    pub files: Vec<GeneratedFile>,
47}
48
49impl GeneratedCode {
50    /// Creates a new empty generated code container.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use mcp_execution_codegen::GeneratedCode;
56    ///
57    /// let code = GeneratedCode::new();
58    /// assert_eq!(code.file_count(), 0);
59    /// ```
60    #[inline]
61    #[must_use]
62    pub const fn new() -> Self {
63        Self { files: Vec::new() }
64    }
65
66    /// Adds a generated file to the collection.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`Error::DuplicateGeneratedFilePath`] if `file.path` is already present in
71    /// this collection. Silently overwriting an existing entry would discard the file
72    /// already added at that path with no signal to the caller (issue #312); every path
73    /// added here is expected to already be unique by construction, so this only fires
74    /// when that invariant has actually been violated upstream.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
80    ///
81    /// let mut code = GeneratedCode::new();
82    /// code.add_file(GeneratedFile {
83    ///     path: "index.ts".to_string(),
84    ///     content: "export {}".to_string(),
85    /// })
86    /// .unwrap();
87    ///
88    /// assert_eq!(code.file_count(), 1);
89    ///
90    /// let err = code
91    ///     .add_file(GeneratedFile {
92    ///         path: "index.ts".to_string(),
93    ///         content: "export const x = 1;".to_string(),
94    ///     })
95    ///     .unwrap_err();
96    /// assert!(err.is_duplicate_generated_file_path());
97    /// ```
98    pub fn add_file(&mut self, file: GeneratedFile) -> Result<()> {
99        if self.files.iter().any(|existing| existing.path == file.path) {
100            return Err(Error::DuplicateGeneratedFilePath { path: file.path });
101        }
102        self.files.push(file);
103        Ok(())
104    }
105
106    /// Returns the number of generated files.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use mcp_execution_codegen::GeneratedCode;
112    ///
113    /// let code = GeneratedCode::new();
114    /// assert_eq!(code.file_count(), 0);
115    /// ```
116    #[inline]
117    #[must_use]
118    pub const fn file_count(&self) -> usize {
119        self.files.len()
120    }
121
122    /// Returns an iterator over the generated files.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
128    ///
129    /// let mut code = GeneratedCode::new();
130    /// code.add_file(GeneratedFile {
131    ///     path: "test.ts".to_string(),
132    ///     content: "content".to_string(),
133    /// });
134    ///
135    /// for file in code.files() {
136    ///     println!("Path: {}", file.path);
137    /// }
138    /// ```
139    #[inline]
140    pub fn files(&self) -> impl Iterator<Item = &GeneratedFile> {
141        self.files.iter()
142    }
143}
144
145impl Default for GeneratedCode {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// A single generated file with path and content.
152///
153/// Represents one file that will be written to the virtual filesystem
154/// or actual filesystem during code generation.
155///
156/// # Examples
157///
158/// ```
159/// use mcp_execution_codegen::GeneratedFile;
160///
161/// let file = GeneratedFile {
162///     path: "types.ts".to_string(),
163///     content: "export type Params = {};".to_string(),
164/// };
165///
166/// assert_eq!(file.path, "types.ts");
167/// ```
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct GeneratedFile {
170    /// Relative path where the file should be written
171    pub path: String,
172    /// File content
173    pub content: String,
174}
175
176impl GeneratedFile {
177    /// Returns the file path.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use mcp_execution_codegen::GeneratedFile;
183    ///
184    /// let file = GeneratedFile {
185    ///     path: "test.ts".to_string(),
186    ///     content: String::new(),
187    /// };
188    ///
189    /// assert_eq!(file.path(), "test.ts");
190    /// ```
191    #[inline]
192    #[must_use]
193    pub fn path(&self) -> &str {
194        &self.path
195    }
196
197    /// Returns the file content.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use mcp_execution_codegen::GeneratedFile;
203    ///
204    /// let file = GeneratedFile {
205    ///     path: "test.ts".to_string(),
206    ///     content: "export {}".to_string(),
207    /// };
208    ///
209    /// assert_eq!(file.content(), "export {}");
210    /// ```
211    #[inline]
212    #[must_use]
213    pub fn content(&self) -> &str {
214        &self.content
215    }
216}
217
218/// Template context for code generation.
219///
220/// Contains all the data needed to render a Handlebars template.
221/// This is typically constructed from MCP server information.
222///
223/// # Examples
224///
225/// ```
226/// use mcp_execution_codegen::TemplateContext;
227/// use std::collections::HashMap;
228///
229/// let context = TemplateContext {
230///     server_name: "github".to_string(),
231///     server_version: "1.0.0".to_string(),
232///     tools: vec![],
233///     metadata: HashMap::new(),
234/// };
235///
236/// assert_eq!(context.server_name, "github");
237/// ```
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct TemplateContext {
240    /// Name of the MCP server
241    pub server_name: String,
242    /// Server version string
243    pub server_version: String,
244    /// List of tool definitions
245    pub tools: Vec<ToolDefinition>,
246    /// Additional metadata for template rendering
247    pub metadata: HashMap<String, serde_json::Value>,
248}
249
250/// Definition of a single MCP tool for code generation.
251///
252/// Contains all information needed to generate TypeScript or Rust
253/// code for calling an MCP tool.
254///
255/// # Examples
256///
257/// ```
258/// use mcp_execution_codegen::ToolDefinition;
259/// use serde_json::json;
260///
261/// let tool = ToolDefinition {
262///     name: "send_message".to_string(),
263///     description: "Sends a message".to_string(),
264///     input_schema: json!({"type": "object"}),
265///     typescript_name: "sendMessage".to_string(),
266/// };
267///
268/// assert_eq!(tool.name, "send_message");
269/// assert_eq!(tool.typescript_name, "sendMessage");
270/// ```
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ToolDefinition {
273    /// Original tool name (`snake_case` from MCP)
274    pub name: String,
275    /// Human-readable description
276    pub description: String,
277    /// JSON Schema for input parameters
278    pub input_schema: serde_json::Value,
279    /// TypeScript-friendly name (camelCase)
280    pub typescript_name: String,
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[test]
288    fn test_generated_code_new() {
289        let code = GeneratedCode::new();
290        assert_eq!(code.file_count(), 0);
291    }
292
293    #[test]
294    fn test_generated_code_default() {
295        let code = GeneratedCode::default();
296        assert_eq!(code.file_count(), 0);
297    }
298
299    #[test]
300    fn test_add_file() {
301        let mut code = GeneratedCode::new();
302        code.add_file(GeneratedFile {
303            path: "test.ts".to_string(),
304            content: "content".to_string(),
305        })
306        .unwrap();
307        assert_eq!(code.file_count(), 1);
308    }
309
310    #[test]
311    fn test_add_file_rejects_duplicate_path() {
312        let mut code = GeneratedCode::new();
313        code.add_file(GeneratedFile {
314            path: "index.ts".to_string(),
315            content: "first".to_string(),
316        })
317        .unwrap();
318
319        let err = code
320            .add_file(GeneratedFile {
321                path: "index.ts".to_string(),
322                content: "second".to_string(),
323            })
324            .unwrap_err();
325
326        assert!(err.is_duplicate_generated_file_path());
327        // The original file must be left untouched, not silently overwritten.
328        assert_eq!(code.file_count(), 1);
329        assert_eq!(code.files[0].content, "first");
330    }
331
332    #[test]
333    fn test_tool_definition() {
334        let tool = ToolDefinition {
335            name: "test_tool".to_string(),
336            description: "Test".to_string(),
337            input_schema: serde_json::json!({"type": "object"}),
338            typescript_name: "testTool".to_string(),
339        };
340
341        assert_eq!(tool.name, "test_tool");
342        assert_eq!(tool.typescript_name, "testTool");
343    }
344
345    #[test]
346    fn test_template_context() {
347        let context = TemplateContext {
348            server_name: "test-server".to_string(),
349            server_version: "1.0.0".to_string(),
350            tools: vec![],
351            metadata: HashMap::new(),
352        };
353
354        assert_eq!(context.server_name, "test-server");
355        assert_eq!(context.tools.len(), 0);
356    }
357}