use schemars::{schema_for, JsonSchema};
use serde_json::Value;
pub struct TypeScriptGenerator;
impl TypeScriptGenerator {
pub fn generate<T: JsonSchema>() -> String {
let schema = schema_for!(T);
let schema_value = serde_json::to_value(schema).unwrap_or_else(|e| {
eprintln!("Failed to convert schema to JSON value: {}", e);
serde_json::Value::Null
});
Self::convert_json_schema_to_typescript(&schema_value)
}
fn convert_json_schema_to_typescript(schema: &Value) -> String {
let mut interfaces = Vec::new();
if let Some(definitions) = schema.get("definitions") {
if let Some(defs_obj) = definitions.as_object() {
for (name, def_schema) in defs_obj {
let interface = Self::generate_interface(name, def_schema);
interfaces.push(interface);
}
}
}
if let Some(_properties) = schema.get("properties") {
let interface_name = schema
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("Config");
let main_interface = Self::generate_interface(interface_name, schema);
interfaces.push(main_interface);
}
if interfaces.is_empty() {
"// Invalid schema format".to_string()
} else {
interfaces.join("\n\n")
}
}
fn generate_interface(name: &str, schema: &Value) -> String {
if let Some(_one_of) = schema.get("oneOf").and_then(|o| o.as_array()) {
let union_type = Self::get_typescript_type(schema);
return format!("export type {} = {};", name, union_type);
}
let mut properties = Vec::new();
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
for (prop_name, prop_schema) in props {
let prop_type = Self::get_typescript_type(prop_schema);
let optional = Self::is_optional(prop_name, schema);
let property_def = if optional {
format!(" {}?: {};", prop_name, prop_type)
} else {
format!(" {}: {};", prop_name, prop_type)
};
properties.push(property_def);
}
} else if let Some(type_str) = schema.get("type").and_then(|t| t.as_str()) {
if type_str != "object" {
let ts_type = Self::get_typescript_type(schema);
return format!("export type {} = {};", name, ts_type);
}
}
let properties_str = properties.join("\n");
format!("export interface {} {{\n{}\n}}", name, properties_str)
}
fn get_typescript_type(schema: &Value) -> String {
if let Some(ref_name) = schema.get("$ref").and_then(|r| r.as_str()) {
let parts: Vec<&str> = ref_name.split('/').collect();
return parts.last().unwrap_or(&"any").to_string();
}
if let Some(type_array) = schema.get("type").and_then(|t| t.as_array()) {
let types: Vec<String> = type_array
.iter()
.filter_map(|t| t.as_str())
.map(|t| match t {
"string" => "string".to_string(),
"number" | "integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"null" => "null".to_string(),
_ => "any".to_string(),
})
.collect();
if types.len() == 2 && types.contains(&"null".to_string()) {
let non_null_type = types.iter().find(|&t| t != "null");
match non_null_type {
Some(t) => return t.clone(),
None => return "any".to_string(),
}
} else {
return types.join(" | ");
}
}
if let Some(type_str) = schema.get("type").and_then(|t| t.as_str()) {
match type_str {
"string" => "string".to_string(),
"number" | "integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"array" => {
if let Some(items) = schema.get("items") {
let item_type = Self::get_typescript_type(items);
format!("{}[]", item_type)
} else {
"any[]".to_string()
}
}
"object" => {
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
let mut inner_props = Vec::new();
for (p_name, p_schema) in props {
let p_type = Self::get_typescript_type(p_schema);
inner_props.push(format!("{}: {}", p_name, p_type));
}
format!("{{ {} }}", inner_props.join("; "))
} else if let Some(additional_props) = schema.get("additionalProperties") {
let val_type = Self::get_typescript_type(additional_props);
format!("Record<string, {}>", val_type)
} else {
"Record<string, any>".to_string()
}
}
_ => "any".to_string(),
}
} else if let Some(any_of) = schema.get("anyOf").and_then(|a| a.as_array()) {
let union_types: Vec<String> = any_of
.iter()
.map(Self::get_typescript_type)
.filter(|t| t != "null")
.collect();
if union_types.is_empty() {
"any".to_string()
} else if union_types.len() == 1 {
union_types[0].clone()
} else {
union_types.join(" | ")
}
} else if let Some(one_of) = schema.get("oneOf").and_then(|o| o.as_array()) {
let mut union_types = Vec::new();
for variant_schema in one_of {
let variant_type = Self::get_typescript_type(variant_schema);
if variant_type != "any" {
union_types.push(variant_type);
}
}
if union_types.is_empty() {
"any".to_string()
} else {
union_types.join(" | ")
}
} else if let Some(all_of) = schema.get("allOf").and_then(|a| a.as_array()) {
let all_types: Vec<String> = all_of
.iter()
.map(Self::get_typescript_type)
.filter(|t| t != "any")
.collect();
if all_types.is_empty() {
"any".to_string()
} else {
all_types.join(" & ")
}
} else if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let variants: Vec<String> = enum_values
.iter()
.map(|v| match v {
Value::String(s) => format!("\"{}\"", s),
_ => v.to_string(),
})
.collect();
if !variants.is_empty() {
variants.join(" | ")
} else {
"any".to_string()
}
} else {
"any".to_string()
}
}
fn is_optional(property_name: &str, schema: &Value) -> bool {
if let Some(required) = schema.get("required").and_then(|r| r.as_array()) {
!required.iter().any(|r| r.as_str() == Some(property_name))
} else {
true }
}
pub fn from_json_value(value: &Value) -> String {
match value {
Value::Object(map) => Self::generate_from_object(map),
Value::Array(arr) => {
if let Some(first) = arr.first() {
format!("{}[]", Self::from_json_value(first))
} else {
"any[]".to_string()
}
}
Value::String(_) => "string".to_string(),
Value::Number(_) => "number".to_string(),
Value::Bool(_) => "boolean".to_string(),
Value::Null => "null".to_string(),
}
}
fn generate_from_object(obj: &serde_json::Map<String, Value>) -> String {
let mut properties = Vec::new();
for (key, value) in obj {
let prop_type = Self::from_json_value(value);
properties.push(format!(" {}: {};", key, prop_type));
}
if properties.is_empty() {
"Record<string, any>".to_string()
} else {
format!("{{\n{}\n}}", properties.join("\n"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct NestedConfig {
description: String,
value: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct TestConfig {
name: String,
count: u32,
enabled: bool,
tags: Vec<String>,
nested: Option<NestedConfig>,
config_type: ConfigType,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
enum ConfigType {
Basic,
Advanced,
Custom(String),
}
#[test]
fn test_typescript_generation() {
use schemars::schema_for;
let schema = schema_for!(TestConfig);
let schema_json = serde_json::to_string_pretty(&schema).unwrap();
println!("JSON Schema:");
println!("{}", schema_json);
println!("--- End of JSON Schema ---");
let ts_output = TypeScriptGenerator::generate::<TestConfig>();
println!("Generated TypeScript output:");
println!("{}", ts_output);
println!("--- End of output ---");
assert!(!ts_output.is_empty());
}
#[test]
fn test_from_json_value() {
let json = serde_json::json!({
"name": "test",
"count": 42,
"enabled": true,
"tags": ["a", "b"]
});
let ts_type = TypeScriptGenerator::from_json_value(&json);
assert!(ts_type.contains("name: string"));
assert!(ts_type.contains("count: number"));
assert!(ts_type.contains("enabled: boolean"));
assert!(ts_type.contains("tags: string[]"));
}
}