diff --git a/src/typescript.rs b/src/typescript.rs new file mode 100644 index 0000000..a435302 --- /dev/null +++ b/src/typescript.rs @@ -0,0 +1,191 @@ +use serde_json::Value; +use crate::error::{ConfigForgeError, Result}; + +pub fn generate_ts_interface(config: &Value, name: &str) -> Result { + let mut output = String::new(); + output.push_str(&format!("export interface {} {{\n", name.pascal_case())); + let interface_body = value_to_typescript(config, 1); + output.push_str(&interface_body); + output.push_str("}\n"); + Ok(output) +} + +fn value_to_typescript(value: &Value, indent: usize) -> String { + let indent_str = " ".repeat(indent); + match value { + Value::Null => "null".to_string(), + Value::Bool(_) => "boolean".to_string(), + Value::Number(_) => "number".to_string(), + Value::String(_) => "string".to_string(), + Value::Array(arr) => { + if arr.is_empty() { + "any[]".to_string() + } else if arr.len() == 1 { + format!("({})", value_to_typescript(&arr[0], indent)) + } else { + let types: std::collections::HashSet = arr.iter() + .map(|v| value_to_typescript(v, indent)) + .collect(); + if types.len() == 1 { + format!("({})", types.iter().next().unwrap()) + } else { + format!("({})", types.iter().collect::>().join(" | ")) + } + } + } + Value::Object(map) => { + if map.is_empty() { + "Record".to_string() + } else { + let mut result = String::new(); + result.push_str("{\n"); + for (key, value) in map { + let ts_type = value_to_typescript(value, indent + 1); + let optional = key.starts_with('_') || key.ends_with('?'); + let key_name = if key.ends_with('?') { + &key[..key.len() - 1] + } else { + key + }; + if optional { + result.push_str(&format!("{} {}?: {};\n", indent_str, key_name, ts_type)); + } else { + result.push_str(&format!("{} {}: {};\n", indent_str, key_name, ts_type)); + } + } + result.push_str(&format!("{}}}", indent_str)); + result + } + } + } +} + +pub fn schema_to_typescript(schema: &Value, name: &str) -> Result { + let mut output = String::new(); + output.push_str(&format!("export interface {} {{\n", name.pascal_case())); + if let Some(properties) = schema.get("properties") { + let body = schema_properties_to_typescript(properties, 1); + output.push_str(&body); + } + output.push_str("}\n"); + Ok(output) +} + +fn schema_properties_to_typescript(value: &Value, indent: usize) -> String { + let indent_str = " ".repeat(indent); + match value { + Value::Object(map) => { + let mut result = String::new(); + if let Some(props) = map.get("properties") { + if let Some(props_obj) = props.as_object() { + for (key, prop_schema) in props_obj { + let ts_type = schema_type_to_typescript(prop_schema, indent); + let required = if let Some(required_arr) = map.get("required") { + if let Some(required_vec) = required_arr.as_array() { + required_vec.iter().any(|r| r == key) + } else { + false + } + } else { + false + }; + if !required { + result.push_str(&format!("{} {}?: {};\n", indent_str, key, ts_type)); + } else { + result.push_str(&format!("{} {}: {};\n", indent_str, key, ts_type)); + } + } + } + } + result + } + _ => String::new(), + } +} + +fn schema_type_to_typescript(schema: &Value, indent: usize) -> String { + if let Some(type_str) = schema.get("type") { + match type_str.as_str() { + Some("string") => "string".to_string(), + Some("number") => "number".to_string(), + Some("integer") => "number".to_string(), + Some("boolean") => "boolean".to_string(), + Some("null") => "null".to_string(), + Some("array") => { + if let Some(items) = schema.get("items") { + format!("{}", schema_type_to_typescript(items, indent)) + } else { + "any[]".to_string() + } + } + Some("object") => { + if let Some(props) = schema.get("properties") { + let mut result = String::new(); + result.push_str("{\n"); + result.push_str(&schema_properties_to_typescript(props, indent + 1)); + result.push_str(&format!("{}}}\n", " ".repeat(indent))); + result + } else { + "Record".to_string() + } + } + _ => "unknown".to_string(), + } + } else if let Some(any_of) = schema.get("anyOf") { + if let Some(arr) = any_of.as_array() { + let types: Vec = arr.iter() + .map(|s| schema_type_to_typescript(s, indent)) + .collect(); + if types.is_empty() { + "unknown".to_string() + } else { + types.join(" | ") + } + } else { + "unknown".to_string() + } + } else if let Some(one_of) = schema.get("oneOf") { + if let Some(arr) = one_of.as_array() { + let types: Vec = arr.iter() + .map(|s| schema_type_to_typescript(s, indent)) + .collect(); + if types.is_empty() { + "unknown".to_string() + } else { + types.join(" | ") + } + } else { + "unknown".to_string() + } + } else { + "unknown".to_string() + } +} + +trait PascalCase { + fn pascal_case(&self) -> String; +} + +impl PascalCase for str { + fn pascal_case(&self) -> String { + let mut result = String::new(); + let mut capitalize = true; + for c in self.chars() { + if c == '_' || c == '-' || c == ' ' { + capitalize = true; + } else if capitalize { + result.push(c.to_ascii_uppercase()); + capitalize = false; + } else { + result.push(c); + } + } + result + } +} + +impl PascalCase for String { + fn pascal_case(&self) -> String { + self.as_str().pascal_case() + } +}