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    /// use mcp_execution_core::Error;
81    ///
82    /// let mut code = GeneratedCode::new();
83    /// code.add_file(GeneratedFile {
84    ///     path: "index.ts".to_string(),
85    ///     content: "export {}".to_string(),
86    /// })
87    /// .unwrap();
88    ///
89    /// assert_eq!(code.file_count(), 1);
90    ///
91    /// let err = code
92    ///     .add_file(GeneratedFile {
93    ///         path: "index.ts".to_string(),
94    ///         content: "export const x = 1;".to_string(),
95    ///     })
96    ///     .unwrap_err();
97    /// assert!(matches!(err, Error::DuplicateGeneratedFilePath { .. }));
98    /// ```
99    pub fn add_file(&mut self, file: GeneratedFile) -> Result<()> {
100        if self.files.iter().any(|existing| existing.path == file.path) {
101            return Err(Error::DuplicateGeneratedFilePath { path: file.path });
102        }
103        self.files.push(file);
104        Ok(())
105    }
106
107    /// Returns the number of generated files.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// use mcp_execution_codegen::GeneratedCode;
113    ///
114    /// let code = GeneratedCode::new();
115    /// assert_eq!(code.file_count(), 0);
116    /// ```
117    #[inline]
118    #[must_use]
119    pub const fn file_count(&self) -> usize {
120        self.files.len()
121    }
122
123    /// Returns an iterator over the generated files.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use mcp_execution_codegen::{GeneratedCode, GeneratedFile};
129    ///
130    /// let mut code = GeneratedCode::new();
131    /// code.add_file(GeneratedFile {
132    ///     path: "test.ts".to_string(),
133    ///     content: "content".to_string(),
134    /// });
135    ///
136    /// for file in code.files() {
137    ///     println!("Path: {}", file.path);
138    /// }
139    /// ```
140    #[inline]
141    pub fn files(&self) -> impl Iterator<Item = &GeneratedFile> {
142        self.files.iter()
143    }
144}
145
146impl Default for GeneratedCode {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152/// A single generated file with path and content.
153///
154/// Represents one file that will be written to the virtual filesystem
155/// or actual filesystem during code generation.
156///
157/// # Examples
158///
159/// ```
160/// use mcp_execution_codegen::GeneratedFile;
161///
162/// let file = GeneratedFile {
163///     path: "types.ts".to_string(),
164///     content: "export type Params = {};".to_string(),
165/// };
166///
167/// assert_eq!(file.path, "types.ts");
168/// ```
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct GeneratedFile {
171    /// Relative path where the file should be written
172    pub path: String,
173    /// File content
174    pub content: String,
175}
176
177impl GeneratedFile {
178    /// Returns the file path.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use mcp_execution_codegen::GeneratedFile;
184    ///
185    /// let file = GeneratedFile {
186    ///     path: "test.ts".to_string(),
187    ///     content: String::new(),
188    /// };
189    ///
190    /// assert_eq!(file.path(), "test.ts");
191    /// ```
192    #[inline]
193    #[must_use]
194    pub fn path(&self) -> &str {
195        &self.path
196    }
197
198    /// Returns the file content.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use mcp_execution_codegen::GeneratedFile;
204    ///
205    /// let file = GeneratedFile {
206    ///     path: "test.ts".to_string(),
207    ///     content: "export {}".to_string(),
208    /// };
209    ///
210    /// assert_eq!(file.content(), "export {}");
211    /// ```
212    #[inline]
213    #[must_use]
214    pub fn content(&self) -> &str {
215        &self.content
216    }
217}
218
219/// Template context for code generation.
220///
221/// Contains all the data needed to render a Handlebars template.
222/// This is typically constructed from MCP server information.
223///
224/// # Examples
225///
226/// ```
227/// use mcp_execution_codegen::TemplateContext;
228/// use std::collections::HashMap;
229///
230/// let context = TemplateContext {
231///     server_name: "github".to_string(),
232///     server_version: "1.0.0".to_string(),
233///     tools: vec![],
234///     metadata: HashMap::new(),
235/// };
236///
237/// assert_eq!(context.server_name, "github");
238/// ```
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct TemplateContext {
241    /// Name of the MCP server
242    pub server_name: String,
243    /// Server version string
244    pub server_version: String,
245    /// List of tool definitions
246    pub tools: Vec<ToolDefinition>,
247    /// Additional metadata for template rendering
248    pub metadata: HashMap<String, serde_json::Value>,
249}
250
251/// Definition of a single MCP tool for code generation.
252///
253/// Contains all information needed to generate TypeScript or Rust
254/// code for calling an MCP tool.
255///
256/// # Examples
257///
258/// ```
259/// use mcp_execution_codegen::ToolDefinition;
260/// use serde_json::json;
261///
262/// let tool = ToolDefinition {
263///     name: "send_message".to_string(),
264///     description: "Sends a message".to_string(),
265///     input_schema: json!({"type": "object"}),
266///     typescript_name: "sendMessage".to_string(),
267/// };
268///
269/// assert_eq!(tool.name, "send_message");
270/// assert_eq!(tool.typescript_name, "sendMessage");
271/// ```
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ToolDefinition {
274    /// Original tool name (`snake_case` from MCP)
275    pub name: String,
276    /// Human-readable description
277    pub description: String,
278    /// JSON Schema for input parameters
279    pub input_schema: serde_json::Value,
280    /// TypeScript-friendly name (camelCase)
281    pub typescript_name: String,
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn test_generated_code_new() {
290        let code = GeneratedCode::new();
291        assert_eq!(code.file_count(), 0);
292    }
293
294    #[test]
295    fn test_generated_code_default() {
296        let code = GeneratedCode::default();
297        assert_eq!(code.file_count(), 0);
298    }
299
300    #[test]
301    fn test_add_file() {
302        let mut code = GeneratedCode::new();
303        code.add_file(GeneratedFile {
304            path: "test.ts".to_string(),
305            content: "content".to_string(),
306        })
307        .unwrap();
308        assert_eq!(code.file_count(), 1);
309    }
310
311    #[test]
312    fn test_add_file_rejects_duplicate_path() {
313        let mut code = GeneratedCode::new();
314        code.add_file(GeneratedFile {
315            path: "index.ts".to_string(),
316            content: "first".to_string(),
317        })
318        .unwrap();
319
320        let err = code
321            .add_file(GeneratedFile {
322                path: "index.ts".to_string(),
323                content: "second".to_string(),
324            })
325            .unwrap_err();
326
327        assert!(matches!(err, Error::DuplicateGeneratedFilePath { .. }));
328        // The original file must be left untouched, not silently overwritten.
329        assert_eq!(code.file_count(), 1);
330        assert_eq!(code.files[0].content, "first");
331    }
332
333    #[test]
334    fn test_tool_definition() {
335        let tool = ToolDefinition {
336            name: "test_tool".to_string(),
337            description: "Test".to_string(),
338            input_schema: serde_json::json!({"type": "object"}),
339            typescript_name: "testTool".to_string(),
340        };
341
342        assert_eq!(tool.name, "test_tool");
343        assert_eq!(tool.typescript_name, "testTool");
344    }
345
346    #[test]
347    fn test_template_context() {
348        let context = TemplateContext {
349            server_name: "test-server".to_string(),
350            server_version: "1.0.0".to_string(),
351            tools: vec![],
352            metadata: HashMap::new(),
353        };
354
355        assert_eq!(context.server_name, "test-server");
356        assert_eq!(context.tools.len(), 0);
357    }
358}