use regex::Regex;
use serde_json::Value;
pub fn to_kebab_case(name: &str) -> String {
let r1 = Regex::new(r"(.)([A-Z][a-z]+)").unwrap();
let r2 = Regex::new(r"([a-z0-9])([A-Z])").unwrap();
let s1 = r1.replace_all(name, "${1}-${2}");
let s2 = r2.replace_all(&s1, "${1}-${2}");
s2.replace('_', "-").to_lowercase().trim_matches('-').to_string()
}
fn resolve_ts_type(prop_schema: &Value) -> String {
match prop_schema.get("type").and_then(|v| v.as_str()) {
Some("string") => "string".to_string(),
Some("integer") | Some("number") => "number".to_string(),
Some("boolean") => "boolean".to_string(),
Some("array") => {
let items = prop_schema.get("items").unwrap_or(&Value::Null);
let item_type = resolve_ts_type(items);
format!("{}[]", item_type)
}
Some("object") => "Record<string, any>".to_string(),
_ => "any".to_string(),
}
}
fn resolve_wit_type(prop_schema: &Value) -> String {
match prop_schema.get("type").and_then(|v| v.as_str()) {
Some("string") => "string".to_string(),
Some("integer") => "s32".to_string(),
Some("number") => "f64".to_string(),
Some("boolean") => "bool".to_string(),
Some("array") => {
let items = prop_schema.get("items").unwrap_or(&Value::Null);
let item_type = resolve_wit_type(items);
format!("list<{}>", item_type)
}
Some("object") => "string".to_string(), _ => "string".to_string(),
}
}
pub fn generate_typescript_interface(name: &str, schema: &Value) -> Result<String, String> {
let mut lines = Vec::new();
lines.push(format!("export interface {} {{", name));
let properties = match schema.get("properties").and_then(|v| v.as_object()) {
Some(p) => p,
None => return Ok(format!("export interface {} {{\n}}\n", name)),
};
let required_set: std::collections::HashSet<&str> = schema
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|val| val.as_str())
.collect()
})
.unwrap_or_default();
for (prop_name, prop_schema) in properties {
let is_required = required_set.contains(prop_name.as_str());
let optional_marker = if is_required { "" } else { "?" };
let ts_type = resolve_ts_type(prop_schema);
if let Some(desc) = prop_schema.get("description").and_then(|v| v.as_str()) {
if !desc.is_empty() {
lines.push(format!(" /** {} */", desc));
}
}
lines.push(format!(" {}{}: {};", prop_name, optional_marker, ts_type));
}
lines.push("}".to_string());
Ok(lines.join("\n") + "\n")
}
pub fn generate_wit_record(name: &str, schema: &Value) -> Result<String, String> {
let kebab_name = to_kebab_case(name);
let mut lines = Vec::new();
lines.push(format!("record {} {{", kebab_name));
let properties = match schema.get("properties").and_then(|v| v.as_object()) {
Some(p) => p,
None => return Ok(format!("record {} {{\n}}\n", kebab_name)),
};
let required_set: std::collections::HashSet<&str> = schema
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|val| val.as_str())
.collect()
})
.unwrap_or_default();
for (prop_name, prop_schema) in properties {
let is_required = required_set.contains(prop_name.as_str());
let mut wit_type = resolve_wit_type(prop_schema);
if !is_required {
wit_type = format!("option<{}>", wit_type);
}
let kebab_prop_name = to_kebab_case(prop_name);
lines.push(format!(" {}: {},", kebab_prop_name, wit_type));
}
lines.push("}".to_string());
Ok(lines.join("\n") + "\n")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_to_kebab_case() {
assert_eq!(to_kebab_case("PascalCase"), "pascal-case");
assert_eq!(to_kebab_case("camelCase"), "camel-case");
assert_eq!(to_kebab_case("snake_case"), "snake-case");
assert_eq!(to_kebab_case("kebab-case"), "kebab-case");
assert_eq!(to_kebab_case("actionspace:node:test_agent"), "actionspace:node:test-agent");
}
#[test]
fn test_generate_typescript_interface() {
let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User name"
},
"age": {
"type": "integer"
}
},
"required": ["name"]
});
let ts = generate_typescript_interface("User", &schema).unwrap();
assert!(ts.contains("export interface User"));
assert!(ts.contains("name: string;"));
assert!(ts.contains("age?: number;"));
assert!(ts.contains("User name"));
}
#[test]
fn test_generate_wit_record() {
let schema = json!({
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"isActive": {
"type": "boolean"
}
},
"required": ["firstName"]
});
let wit = generate_wit_record("UserRecord", &schema).unwrap();
assert!(wit.contains("record user-record"));
assert!(wit.contains("first-name: string,"));
assert!(wit.contains("is-active: option<bool>,"));
}
}