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}