use crate::schema::{AutoGenerationRule, CascadeAction, ForeignKeyDefinition, VbrSchemaDefinition};
use crate::{Error, Result};
use mockforge_core::openapi::OpenApiSpec;
use mockforge_data::schema::{FieldDefinition, SchemaDefinition};
use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
use std::collections::HashMap;
#[derive(Debug)]
pub struct OpenApiConversionResult {
pub entities: Vec<(String, VbrSchemaDefinition)>,
pub warnings: Vec<String>,
}
pub fn convert_openapi_to_vbr(spec: &OpenApiSpec) -> Result<OpenApiConversionResult> {
let mut entities = Vec::new();
let mut warnings = Vec::new();
let schemas = extract_schemas_from_openapi(spec);
if schemas.is_empty() {
warnings.push("No schemas found in OpenAPI components/schemas".to_string());
return Ok(OpenApiConversionResult { entities, warnings });
}
let schema_names: Vec<String> = schemas.keys().cloned().collect();
for schema_name in schema_names {
let schema = schemas.get(&schema_name).unwrap().clone();
match convert_schema_to_vbr(&schema_name, schema, &schemas) {
Ok(vbr_schema) => {
entities.push((schema_name.clone(), vbr_schema));
}
Err(e) => {
warnings.push(format!("Failed to convert schema '{}': {}", schema_name, e));
}
}
}
let entity_names: Vec<String> = entities.iter().map(|(n, _)| n.clone()).collect();
for (entity_name, vbr_schema) in &mut entities {
detect_foreign_keys(entity_name, vbr_schema, &entity_names, &mut warnings);
}
Ok(OpenApiConversionResult { entities, warnings })
}
fn extract_schemas_from_openapi(spec: &OpenApiSpec) -> HashMap<String, Schema> {
let mut schemas = HashMap::new();
if let Some(components) = &spec.spec.components {
if !components.schemas.is_empty() {
for (name, schema_ref) in &components.schemas {
match schema_ref {
ReferenceOr::Item(schema) => {
schemas.insert(name.clone(), schema.clone());
}
ReferenceOr::Reference { reference } => {
if let Some(resolved) = spec.get_schema(reference) {
schemas.insert(name.clone(), resolved.schema);
}
}
}
}
}
}
schemas
}
fn resolve_schema_reference(
reference: &str,
all_schemas: &HashMap<String, Schema>,
) -> Option<Schema> {
let schema_name = reference.strip_prefix("#/components/schemas/")?;
all_schemas.get(schema_name).cloned()
}
fn convert_schema_to_vbr(
schema_name: &str,
schema: Schema,
all_schemas: &HashMap<String, Schema>,
) -> Result<VbrSchemaDefinition> {
let mut fields = Vec::new();
let mut primary_key = Vec::new();
let mut auto_generation = HashMap::new();
if let SchemaKind::Type(Type::Object(obj_type)) = &schema.schema_kind {
for (field_name, field_schema_ref) in &obj_type.properties {
match field_schema_ref {
ReferenceOr::Item(field_schema) => {
let field_def =
convert_field_to_definition(field_name, field_schema, &obj_type.required)?;
fields.push(field_def.clone());
if is_primary_key_field(field_name, &field_def) {
primary_key.push(field_name.clone());
if primary_key.len() == 1 && !auto_generation.contains_key(field_name) {
auto_generation.insert(field_name.clone(), AutoGenerationRule::Uuid);
}
}
if let Some(rule) = detect_auto_generation(field_name, field_schema) {
auto_generation.insert(field_name.clone(), rule);
}
}
ReferenceOr::Reference { reference } => {
if let Some(resolved_schema) = resolve_schema_reference(reference, all_schemas)
{
match convert_field_to_definition(
field_name,
&resolved_schema,
&obj_type.required,
) {
Ok(field_def) => {
fields.push(field_def.clone());
if is_primary_key_field(field_name, &field_def) {
primary_key.push(field_name.clone());
if primary_key.len() == 1
&& !auto_generation.contains_key(field_name)
{
auto_generation
.insert(field_name.clone(), AutoGenerationRule::Uuid);
}
}
if let Some(rule) =
detect_auto_generation(field_name, &resolved_schema)
{
auto_generation.insert(field_name.clone(), rule);
}
}
Err(_e) => {
let field_def =
FieldDefinition::new(field_name.clone(), "string".to_string())
.optional();
fields.push(field_def);
}
}
} else {
let field_def =
FieldDefinition::new(field_name.clone(), "string".to_string())
.optional();
fields.push(field_def);
}
}
}
}
} else {
return Err(Error::generic(format!(
"Schema '{}' is not an object type, cannot convert to entity",
schema_name
)));
}
if primary_key.is_empty() {
if fields.iter().any(|f| f.name == "id") {
primary_key.push("id".to_string());
auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
} else {
primary_key.push("id".to_string());
fields.insert(
0,
FieldDefinition::new("id".to_string(), "string".to_string())
.with_description("Auto-generated primary key".to_string()),
);
auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
}
}
let base_schema = SchemaDefinition {
name: schema_name.to_string(),
fields,
description: schema.schema_data.description.clone(),
metadata: HashMap::new(),
relationships: HashMap::new(),
};
let vbr_schema = VbrSchemaDefinition::new(base_schema).with_primary_key(primary_key);
let mut final_schema = vbr_schema;
for (field, rule) in auto_generation {
final_schema = final_schema.with_auto_generation(field, rule);
}
Ok(final_schema)
}
fn convert_field_to_definition(
field_name: &str,
schema: &Schema,
required_fields: &[String],
) -> Result<FieldDefinition> {
let required = required_fields.contains(&field_name.to_string());
let field_type = schema_type_to_string(schema)?;
let description = schema.schema_data.description.clone();
let mut field_def = FieldDefinition::new(field_name.to_string(), field_type);
if !required {
field_def = field_def.optional();
}
if let Some(desc) = description {
field_def = field_def.with_description(desc);
}
if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
if let Some(max_length) = string_type.max_length {
field_def = field_def.with_constraint("maxLength".to_string(), max_length.into());
}
if let Some(min_length) = string_type.min_length {
field_def = field_def.with_constraint("minLength".to_string(), min_length.into());
}
if let Some(pattern) = &string_type.pattern {
field_def = field_def.with_constraint("pattern".to_string(), pattern.clone().into());
}
} else if let SchemaKind::Type(Type::Integer(int_type)) = &schema.schema_kind {
if let Some(maximum) = int_type.maximum {
field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
}
if let Some(minimum) = int_type.minimum {
field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
}
} else if let SchemaKind::Type(Type::Number(num_type)) = &schema.schema_kind {
if let Some(maximum) = num_type.maximum {
field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
}
if let Some(minimum) = num_type.minimum {
field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
}
}
Ok(field_def)
}
fn schema_type_to_string(schema: &Schema) -> Result<String> {
match &schema.schema_kind {
SchemaKind::Type(Type::String(string_type)) => {
match &string_type.format {
openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
openapiv3::StringFormat::Date => Ok("date".to_string()),
openapiv3::StringFormat::DateTime => Ok("datetime".to_string()),
_ => Ok("string".to_string()),
},
_ => Ok("string".to_string()),
}
}
SchemaKind::Type(Type::Integer(_)) => Ok("integer".to_string()),
SchemaKind::Type(Type::Number(_)) => Ok("number".to_string()),
SchemaKind::Type(Type::Boolean(_)) => Ok("boolean".to_string()),
SchemaKind::Type(Type::Array(_)) => Ok("array".to_string()),
SchemaKind::Type(Type::Object(_)) => Ok("object".to_string()),
_ => Ok("string".to_string()), }
}
fn is_primary_key_field(field_name: &str, field_def: &FieldDefinition) -> bool {
let pk_names = ["id", "uuid", "_id", "pk"];
if pk_names.contains(&field_name.to_lowercase().as_str()) {
return true;
}
if field_def.required {
false
} else {
false
}
}
fn detect_auto_generation(field_name: &str, schema: &Schema) -> Option<AutoGenerationRule> {
let name_lower = field_name.to_lowercase();
if name_lower.contains("uuid") || name_lower == "id" {
if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
if let openapiv3::VariantOrUnknownOrEmpty::Item(_) = &string_type.format {
}
}
return Some(AutoGenerationRule::Uuid);
}
if name_lower.contains("timestamp")
|| name_lower.contains("created_at")
|| name_lower.contains("updated_at")
{
return Some(AutoGenerationRule::Timestamp);
}
if name_lower.contains("date") && !name_lower.contains("timestamp") {
if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
if let openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Date) =
&string_type.format
{
return Some(AutoGenerationRule::Date);
}
}
}
None
}
fn detect_foreign_keys(
_entity_name: &str,
vbr_schema: &mut VbrSchemaDefinition,
entity_names: &[String],
_warnings: &mut [String],
) {
for field in &vbr_schema.base.fields {
if is_foreign_key_field(&field.name, entity_names) {
if let Some(target_entity) = extract_target_entity(&field.name, entity_names) {
if !vbr_schema.foreign_keys.iter().any(|fk| fk.field == field.name) {
let fk = ForeignKeyDefinition {
field: field.name.clone(),
target_entity: target_entity.clone(),
target_field: "id".to_string(), on_delete: CascadeAction::NoAction,
on_update: CascadeAction::NoAction,
};
vbr_schema.foreign_keys.push(fk);
}
}
}
}
}
fn is_foreign_key_field(field_name: &str, entity_names: &[String]) -> bool {
let name_lower = field_name.to_lowercase();
if name_lower.ends_with("_id") {
return true;
}
for entity_name in entity_names {
let entity_lower = entity_name.to_lowercase();
if name_lower == entity_lower
|| name_lower == format!("{}_id", entity_lower)
|| name_lower == format!("{}id", entity_lower)
{
return true;
}
}
false
}
fn extract_target_entity(field_name: &str, entity_names: &[String]) -> Option<String> {
let name_lower = field_name.to_lowercase();
let base_name = name_lower.trim_end_matches("_id").trim_end_matches("id").to_string();
for entity_name in entity_names {
let entity_lower = entity_name.to_lowercase();
if base_name == entity_lower || name_lower == format!("{}_id", entity_lower) {
return Some(entity_name.clone());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use mockforge_core::openapi::OpenApiSpec;
#[test]
fn test_extract_schemas() {
let spec_json = serde_json::json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
},
"email": {
"type": "string",
"format": "email"
}
},
"required": ["id", "name", "email"]
}
}
},
"paths": {}
});
let spec = OpenApiSpec::from_json(spec_json).unwrap();
let schemas = extract_schemas_from_openapi(&spec);
assert_eq!(schemas.len(), 1);
assert!(schemas.contains_key("User"));
}
#[test]
fn test_convert_schema_to_vbr() {
let spec_json = serde_json::json!({
"openapi": "3.0.0",
"info": {
"title": "Test API",
"version": "1.0.0"
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"name": {
"type": "string"
}
},
"required": ["id", "name"]
}
}
},
"paths": {}
});
let spec = OpenApiSpec::from_json(spec_json).unwrap();
let schemas = extract_schemas_from_openapi(&spec);
let user_schema = schemas.get("User").unwrap();
let result = convert_schema_to_vbr("User", user_schema.clone(), &schemas);
assert!(result.is_ok());
let vbr_schema = result.unwrap();
assert_eq!(vbr_schema.primary_key, vec!["id"]);
assert_eq!(vbr_schema.base.fields.len(), 2);
assert!(vbr_schema.auto_generation.contains_key("id"));
}
}