mcp_execution_codegen/common/
typescript.rs

1//! TypeScript code generation utilities.
2//!
3//! Provides functions to convert JSON Schema to TypeScript types
4//! and generate type-safe TypeScript code.
5//!
6//! # Examples
7//!
8//! ```
9//! use mcp_execution_codegen::common::typescript;
10//! use serde_json::json;
11//!
12//! let schema = json!({
13//!     "type": "object",
14//!     "properties": {
15//!         "name": {"type": "string"},
16//!         "age": {"type": "number"}
17//!     }
18//! });
19//!
20//! let ts_type = typescript::json_schema_to_typescript(&schema);
21//! ```
22
23use serde_json::Value;
24
25/// Converts a snake_case name to camelCase for TypeScript.
26///
27/// # Examples
28///
29/// ```
30/// use mcp_execution_codegen::common::typescript::to_camel_case;
31///
32/// assert_eq!(to_camel_case("send_message"), "sendMessage");
33/// assert_eq!(to_camel_case("get_user_data"), "getUserData");
34/// assert_eq!(to_camel_case("hello"), "hello");
35/// ```
36#[must_use]
37pub fn to_camel_case(snake_case: &str) -> String {
38    let mut result = String::new();
39    let mut capitalize_next = false;
40
41    for ch in snake_case.chars() {
42        if ch == '_' {
43            capitalize_next = true;
44        } else if capitalize_next {
45            result.push(ch.to_ascii_uppercase());
46            capitalize_next = false;
47        } else {
48            result.push(ch);
49        }
50    }
51
52    result
53}
54
55/// Converts a snake_case name to PascalCase for TypeScript types.
56///
57/// # Examples
58///
59/// ```
60/// use mcp_execution_codegen::common::typescript::to_pascal_case;
61///
62/// assert_eq!(to_pascal_case("send_message"), "SendMessage");
63/// assert_eq!(to_pascal_case("get_user_data"), "GetUserData");
64/// assert_eq!(to_pascal_case("hello"), "Hello");
65/// ```
66#[must_use]
67pub fn to_pascal_case(snake_case: &str) -> String {
68    let camel = to_camel_case(snake_case);
69    let mut chars = camel.chars();
70    match chars.next() {
71        None => String::new(),
72        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
73    }
74}
75
76/// Converts JSON Schema type to TypeScript type.
77///
78/// Maps JSON Schema primitive types to their TypeScript equivalents.
79///
80/// # Examples
81///
82/// ```
83/// use mcp_execution_codegen::common::typescript::json_type_to_typescript;
84///
85/// assert_eq!(json_type_to_typescript("string"), "string");
86/// assert_eq!(json_type_to_typescript("number"), "number");
87/// assert_eq!(json_type_to_typescript("integer"), "number");
88/// assert_eq!(json_type_to_typescript("boolean"), "boolean");
89/// assert_eq!(json_type_to_typescript("unknown_type"), "unknown");
90/// ```
91#[must_use]
92pub fn json_type_to_typescript(json_type: &str) -> &'static str {
93    match json_type {
94        "string" => "string",
95        "number" | "integer" => "number",
96        "boolean" => "boolean",
97        "array" => "unknown[]",
98        "object" => "Record<string, unknown>",
99        "null" => "null",
100        _ => "unknown",
101    }
102}
103
104/// Converts a JSON Schema to TypeScript type definition.
105///
106/// Handles complex schemas including objects, arrays, and nested types.
107///
108/// # Examples
109///
110/// ```
111/// use mcp_execution_codegen::common::typescript::json_schema_to_typescript;
112/// use serde_json::json;
113///
114/// let schema = json!({
115///     "type": "object",
116///     "properties": {
117///         "name": {"type": "string"},
118///         "age": {"type": "number"}
119///     },
120///     "required": ["name"]
121/// });
122///
123/// let ts = json_schema_to_typescript(&schema);
124/// assert!(ts.contains("name: string"));
125/// ```
126#[must_use]
127pub fn json_schema_to_typescript(schema: &Value) -> String {
128    match schema {
129        Value::Object(obj) => {
130            // Get type field
131            let schema_type = obj
132                .get("type")
133                .and_then(|v| v.as_str())
134                .unwrap_or("unknown");
135
136            match schema_type {
137                "object" => {
138                    // Extract properties
139                    let properties = obj.get("properties").and_then(|v| v.as_object());
140                    let required = obj
141                        .get("required")
142                        .and_then(|v| v.as_array())
143                        .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
144                        .unwrap_or_default();
145
146                    if let Some(props) = properties {
147                        let mut fields = Vec::new();
148                        for (key, value) in props {
149                            let is_required = required.contains(&key.as_str());
150                            let optional_marker = if is_required { "" } else { "?" };
151                            let ts_type = json_schema_to_typescript(value);
152                            fields.push(format!("  {}{}: {};", key, optional_marker, ts_type));
153                        }
154
155                        if fields.is_empty() {
156                            "Record<string, unknown>".to_string()
157                        } else {
158                            format!("{{\n{}\n}}", fields.join("\n"))
159                        }
160                    } else {
161                        "Record<string, unknown>".to_string()
162                    }
163                }
164                "array" => {
165                    let items = obj.get("items");
166                    if let Some(item_schema) = items {
167                        format!("{}[]", json_schema_to_typescript(item_schema))
168                    } else {
169                        "unknown[]".to_string()
170                    }
171                }
172                other => json_type_to_typescript(other).to_string(),
173            }
174        }
175        Value::String(s) => json_type_to_typescript(s).to_string(),
176        _ => "unknown".to_string(),
177    }
178}
179
180/// Extracts property definitions from JSON Schema for template rendering.
181///
182/// Returns a vector of property information suitable for Handlebars templates.
183///
184/// # Examples
185///
186/// ```
187/// use mcp_execution_codegen::common::typescript::extract_properties;
188/// use serde_json::json;
189///
190/// let schema = json!({
191///     "type": "object",
192///     "properties": {
193///         "name": {"type": "string"},
194///         "age": {"type": "number"}
195///     },
196///     "required": ["name"]
197/// });
198///
199/// let props = extract_properties(&schema);
200/// assert_eq!(props.len(), 2);
201/// ```
202#[must_use]
203pub fn extract_properties(schema: &Value) -> Vec<serde_json::Value> {
204    let mut properties = Vec::new();
205
206    if let Some(obj) = schema.as_object()
207        && let Some(props) = obj.get("properties").and_then(|v| v.as_object())
208    {
209        let required = obj
210            .get("required")
211            .and_then(|v| v.as_array())
212            .map(|arr| {
213                arr.iter()
214                    .filter_map(|v| v.as_str())
215                    .map(String::from)
216                    .collect::<Vec<_>>()
217            })
218            .unwrap_or_default();
219
220        for (name, prop_schema) in props {
221            let ts_type = json_schema_to_typescript(prop_schema);
222            let is_required = required.contains(name);
223
224            properties.push(serde_json::json!({
225                "name": name,
226                "type": ts_type,
227                "required": is_required,
228            }));
229        }
230    }
231
232    properties
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use serde_json::json;
239
240    #[test]
241    fn test_to_camel_case() {
242        assert_eq!(to_camel_case("send_message"), "sendMessage");
243        assert_eq!(to_camel_case("get_user_data"), "getUserData");
244        assert_eq!(to_camel_case("hello"), "hello");
245        assert_eq!(to_camel_case("a_b_c"), "aBC");
246    }
247
248    #[test]
249    fn test_to_pascal_case() {
250        assert_eq!(to_pascal_case("send_message"), "SendMessage");
251        assert_eq!(to_pascal_case("get_user_data"), "GetUserData");
252        assert_eq!(to_pascal_case("hello"), "Hello");
253    }
254
255    #[test]
256    fn test_json_type_to_typescript() {
257        assert_eq!(json_type_to_typescript("string"), "string");
258        assert_eq!(json_type_to_typescript("number"), "number");
259        assert_eq!(json_type_to_typescript("integer"), "number");
260        assert_eq!(json_type_to_typescript("boolean"), "boolean");
261        assert_eq!(json_type_to_typescript("array"), "unknown[]");
262        assert_eq!(json_type_to_typescript("object"), "Record<string, unknown>");
263        assert_eq!(json_type_to_typescript("null"), "null");
264        assert_eq!(json_type_to_typescript("unknown_type"), "unknown");
265    }
266
267    #[test]
268    fn test_json_schema_to_typescript_primitive() {
269        assert_eq!(
270            json_schema_to_typescript(&json!({"type": "string"})),
271            "string"
272        );
273        assert_eq!(
274            json_schema_to_typescript(&json!({"type": "number"})),
275            "number"
276        );
277    }
278
279    #[test]
280    fn test_json_schema_to_typescript_object() {
281        let schema = json!({
282            "type": "object",
283            "properties": {
284                "name": {"type": "string"},
285                "age": {"type": "number"}
286            },
287            "required": ["name"]
288        });
289
290        let result = json_schema_to_typescript(&schema);
291        assert!(result.contains("name: string"));
292        assert!(result.contains("age?: number"));
293    }
294
295    #[test]
296    fn test_json_schema_to_typescript_array() {
297        let schema = json!({
298            "type": "array",
299            "items": {"type": "string"}
300        });
301
302        assert_eq!(json_schema_to_typescript(&schema), "string[]");
303    }
304
305    #[test]
306    fn test_extract_properties() {
307        let schema = json!({
308            "type": "object",
309            "properties": {
310                "name": {"type": "string"},
311                "age": {"type": "number"}
312            },
313            "required": ["name"]
314        });
315
316        let props = extract_properties(&schema);
317        assert_eq!(props.len(), 2);
318
319        // Find the "name" property (HashMap order is not guaranteed)
320        let name_prop = props
321            .iter()
322            .find(|p| p["name"] == "name")
323            .expect("name property not found");
324
325        assert_eq!(name_prop["type"], "string");
326        assert_eq!(name_prop["required"], true);
327
328        // Check age property
329        let age_prop = props
330            .iter()
331            .find(|p| p["name"] == "age")
332            .expect("age property not found");
333
334        assert_eq!(age_prop["type"], "number");
335        assert_eq!(age_prop["required"], false);
336    }
337
338    #[test]
339    fn test_extract_properties_empty() {
340        let schema = json!({"type": "string"});
341        let props = extract_properties(&schema);
342        assert_eq!(props.len(), 0);
343    }
344}