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}