use crate::error::{Error, Result};
use crate::schema::types::{Field, Schema, SchemaFormat, Type};
pub fn parse_json_schema(name: &str, content: &str) -> Result<Schema> {
let json: serde_json::Value = serde_json::from_str(content).map_err(|e| Error::Schema {
schema_name: name.to_string(),
message: format!("Failed to parse JSON Schema: {}", e),
})?;
let mut schema = Schema::new(name.to_string(), SchemaFormat::JsonSchema);
let properties = json
.get("properties")
.and_then(|p| p.as_object())
.ok_or_else(|| Error::Schema {
schema_name: name.to_string(),
message: "JSON Schema missing 'properties' object".to_string(),
})?;
let required: Vec<String> = json
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
for (field_name, field_spec) in properties {
let field_type = parse_json_schema_type(field_spec)?;
let is_required = required.contains(field_name);
let mut field = Field::new(field_name.clone(), field_type);
field.nullable = !is_required;
if let Some(desc) = field_spec.get("description").and_then(|d| d.as_str()) {
field.doc_comment = Some(desc.to_string());
}
schema.add_field(field);
}
Ok(schema)
}
fn parse_json_schema_type(spec: &serde_json::Value) -> Result<Type> {
let type_str = spec
.get("type")
.and_then(|t| t.as_str())
.ok_or_else(|| Error::Validation {
message: "JSON Schema field missing 'type'".to_string(),
context: Some("parse_json_schema_type".to_string()),
})?;
match type_str {
"string" => {
if let Some(enum_values) = spec.get("enum").and_then(|e| e.as_array()) {
let variants: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
return Ok(Type::Enum(variants));
}
Ok(Type::String)
}
"integer" | "number" => {
if let Some(format) = spec.get("format").and_then(|f| f.as_str()) {
match format {
"float" | "double" => Ok(Type::Float),
_ => Ok(Type::Integer),
}
} else if type_str == "number" {
Ok(Type::Float)
} else {
Ok(Type::Integer)
}
}
"boolean" => Ok(Type::Boolean),
"array" => {
if let Some(items) = spec.get("items") {
let item_type = parse_json_schema_type(items)?;
Ok(Type::Vec(Box::new(item_type)))
} else {
Ok(Type::JsonValue)
}
}
"object" => Ok(Type::JsonValue),
"null" => Ok(Type::String), _ => Ok(Type::Custom(type_str.to_string())),
}
}
pub fn parse_typescript(name: &str, content: &str) -> Result<Schema> {
let mut schema = Schema::new(name.to_string(), SchemaFormat::TypeScript);
let interface_start = content.find("interface").ok_or_else(|| Error::Schema {
schema_name: name.to_string(),
message: "TypeScript content missing 'interface' keyword".to_string(),
})?;
let body_start = content[interface_start..]
.find('{')
.ok_or_else(|| Error::Schema {
schema_name: name.to_string(),
message: "TypeScript interface missing opening '{'".to_string(),
})?
+ interface_start;
let body_end = content[body_start..]
.rfind('}')
.ok_or_else(|| Error::Schema {
schema_name: name.to_string(),
message: "TypeScript interface missing closing '}'".to_string(),
})?
+ body_start;
let body = &content[body_start + 1..body_end];
for line in body.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
if let Some(colon_pos) = line.find(':') {
let name_part = line[..colon_pos].trim();
let type_part = line[colon_pos + 1..].trim().trim_end_matches(';').trim();
let (field_name, is_optional) = if name_part.ends_with('?') {
(name_part.trim_end_matches('?').trim(), true)
} else {
(name_part, false)
};
if field_name.is_empty() {
continue;
}
let field_type = parse_typescript_type(type_part)?;
let mut field = Field::new(field_name.to_string(), field_type);
field.nullable = is_optional;
schema.add_field(field);
}
}
Ok(schema)
}
fn parse_typescript_type(type_str: &str) -> Result<Type> {
let type_str = type_str.trim();
match type_str {
"string" => Ok(Type::String),
"number" => Ok(Type::Float),
"boolean" => Ok(Type::Boolean),
"Date" => Ok(Type::Timestamp),
"any" | "unknown" => Ok(Type::JsonValue),
_ => {
if type_str.ends_with("[]") {
let element_type = type_str.trim_end_matches("[]").trim();
let inner_type = parse_typescript_type(element_type)?;
return Ok(Type::Vec(Box::new(inner_type)));
}
if type_str.starts_with("Array<") && type_str.ends_with('>') {
let element_type = &type_str[6..type_str.len() - 1];
let inner_type = parse_typescript_type(element_type)?;
return Ok(Type::Vec(Box::new(inner_type)));
}
if type_str.contains('|') {
let first_type = type_str.split('|').next().unwrap().trim();
return parse_typescript_type(first_type);
}
Ok(Type::Custom(type_str.to_string()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_json_schema_simple() {
let json = r#"{
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["id", "name"]
}"#;
let schema = parse_json_schema("User", json).unwrap();
assert_eq!(schema.name, "User");
assert_eq!(schema.fields.len(), 3);
assert_eq!(schema.format, SchemaFormat::JsonSchema);
let id_field = schema.get_field("id").unwrap();
assert!(!id_field.nullable);
let age_field = schema.get_field("age").unwrap();
assert!(age_field.nullable); }
#[test]
fn test_parse_json_schema_types() {
let json = r#"{
"type": "object",
"properties": {
"str": {"type": "string"},
"num": {"type": "integer"},
"float": {"type": "number"},
"bool": {"type": "boolean"},
"arr": {"type": "array", "items": {"type": "string"}}
}
}"#;
let schema = parse_json_schema("Types", json).unwrap();
assert_eq!(schema.fields.len(), 5);
assert!(matches!(
schema.get_field("str").unwrap().field_type,
Type::String
));
assert!(matches!(
schema.get_field("num").unwrap().field_type,
Type::Integer
));
assert!(matches!(
schema.get_field("float").unwrap().field_type,
Type::Float
));
assert!(matches!(
schema.get_field("bool").unwrap().field_type,
Type::Boolean
));
}
#[test]
fn test_parse_json_schema_enum() {
let json = r#"{
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
}
}"#;
let schema = parse_json_schema("Model", json).unwrap();
let status_field = schema.get_field("status").unwrap();
if let Type::Enum(variants) = &status_field.field_type {
assert_eq!(variants.len(), 3);
assert!(variants.contains(&"active".to_string()));
} else {
panic!("Expected Enum type");
}
}
#[test]
fn test_parse_typescript_simple() {
let ts = r#"
interface User {
id: string;
name: string;
age: number;
}
"#;
let schema = parse_typescript("User", ts).unwrap();
assert_eq!(schema.name, "User");
assert_eq!(schema.fields.len(), 3);
assert_eq!(schema.format, SchemaFormat::TypeScript);
}
#[test]
fn test_parse_typescript_optional() {
let ts = r#"
interface User {
id: string;
email?: string;
}
"#;
let schema = parse_typescript("User", ts).unwrap();
let id_field = schema.get_field("id").unwrap();
let email_field = schema.get_field("email").unwrap();
assert!(!id_field.nullable);
assert!(email_field.nullable);
}
#[test]
fn test_parse_typescript_types() {
let ts = r#"
interface Types {
str: string;
num: number;
bool: boolean;
date: Date;
arr: string[];
arr2: Array<number>;
}
"#;
let schema = parse_typescript("Types", ts).unwrap();
assert_eq!(schema.fields.len(), 6);
assert!(matches!(
schema.get_field("str").unwrap().field_type,
Type::String
));
assert!(matches!(
schema.get_field("num").unwrap().field_type,
Type::Float
));
assert!(matches!(
schema.get_field("bool").unwrap().field_type,
Type::Boolean
));
assert!(matches!(
schema.get_field("date").unwrap().field_type,
Type::Timestamp
));
if let Type::Vec(inner) = &schema.get_field("arr").unwrap().field_type {
assert!(matches!(**inner, Type::String));
} else {
panic!("Expected Vec type");
}
}
#[test]
fn test_parse_typescript_custom_type() {
let ts = r#"
interface Post {
id: string;
author: User;
}
"#;
let schema = parse_typescript("Post", ts).unwrap();
let author_field = schema.get_field("author").unwrap();
if let Type::Custom(name) = &author_field.field_type {
assert_eq!(name, "User");
} else {
panic!("Expected Custom type");
}
}
#[test]
fn test_parse_json_schema_invalid() {
let json = r#"{"invalid": "json"}"#;
let result = parse_json_schema("Test", json);
assert!(result.is_err());
}
#[test]
fn test_parse_typescript_no_interface() {
let ts = "const x = 5;";
let result = parse_typescript("Test", ts);
assert!(result.is_err());
}
}