Add source files: main, lib, cli, error, convert, highlight, validate, typescript
Some checks failed
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-01-29 15:15:44 +00:00
parent 0aa0c951f6
commit 64888aa9cc

191
src/typescript.rs Normal file
View File

@@ -0,0 +1,191 @@
use serde_json::Value;
use crate::error::{ConfigForgeError, Result};
pub fn generate_ts_interface(config: &Value, name: &str) -> Result<String> {
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<String> = arr.iter()
.map(|v| value_to_typescript(v, indent))
.collect();
if types.len() == 1 {
format!("({})", types.iter().next().unwrap())
} else {
format!("({})", types.iter().collect::<Vec<_>>().join(" | "))
}
}
}
Value::Object(map) => {
if map.is_empty() {
"Record<string, unknown>".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<String> {
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<string, unknown>".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<String> = 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<String> = 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()
}
}