1use crate::schema::{AutoGenerationRule, CascadeAction, ForeignKeyDefinition, VbrSchemaDefinition};
7use crate::{Error, Result};
8use mockforge_core::openapi::OpenApiSpec;
9use mockforge_data::schema::{FieldDefinition, SchemaDefinition};
10use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
11use std::collections::HashMap;
12
13#[derive(Debug)]
15pub struct OpenApiConversionResult {
16 pub entities: Vec<(String, VbrSchemaDefinition)>,
18 pub warnings: Vec<String>,
20}
21
22pub fn convert_openapi_to_vbr(spec: &OpenApiSpec) -> Result<OpenApiConversionResult> {
36 let mut entities = Vec::new();
37 let mut warnings = Vec::new();
38
39 let schemas = extract_schemas_from_openapi(spec);
41
42 if schemas.is_empty() {
43 warnings.push("No schemas found in OpenAPI components/schemas".to_string());
44 return Ok(OpenApiConversionResult { entities, warnings });
45 }
46
47 let schema_names: Vec<String> = schemas.keys().cloned().collect();
50 for schema_name in schema_names {
51 let schema = schemas.get(&schema_name).unwrap().clone();
52 match convert_schema_to_vbr(&schema_name, schema, &schemas) {
53 Ok(vbr_schema) => {
54 entities.push((schema_name.clone(), vbr_schema));
55 }
56 Err(e) => {
57 warnings.push(format!("Failed to convert schema '{}': {}", schema_name, e));
58 }
59 }
60 }
61
62 let entity_names: Vec<String> = entities.iter().map(|(n, _)| n.clone()).collect();
65 for (entity_name, vbr_schema) in &mut entities {
66 detect_foreign_keys(entity_name, vbr_schema, &entity_names, &mut warnings);
67 }
68
69 Ok(OpenApiConversionResult { entities, warnings })
70}
71
72fn extract_schemas_from_openapi(spec: &OpenApiSpec) -> HashMap<String, Schema> {
74 let mut schemas = HashMap::new();
75
76 if let Some(components) = &spec.spec.components {
77 if !components.schemas.is_empty() {
78 for (name, schema_ref) in &components.schemas {
79 if let ReferenceOr::Item(schema) = schema_ref {
80 schemas.insert(name.clone(), schema.clone());
81 }
82 }
83 }
84 }
85
86 schemas
87}
88
89fn convert_schema_to_vbr(
91 schema_name: &str,
92 schema: Schema,
93 all_schemas: &HashMap<String, Schema>,
94) -> Result<VbrSchemaDefinition> {
95 let mut fields = Vec::new();
96 let mut primary_key = Vec::new();
97 let mut auto_generation = HashMap::new();
98
99 if let SchemaKind::Type(Type::Object(obj_type)) = &schema.schema_kind {
101 for (field_name, field_schema_ref) in &obj_type.properties {
103 match field_schema_ref {
104 ReferenceOr::Item(field_schema) => {
105 let field_def =
106 convert_field_to_definition(field_name, field_schema, &obj_type.required)?;
107 fields.push(field_def.clone());
108
109 if is_primary_key_field(field_name, &field_def) {
111 primary_key.push(field_name.clone());
112 if primary_key.len() == 1 && !auto_generation.contains_key(field_name) {
114 auto_generation.insert(field_name.clone(), AutoGenerationRule::Uuid);
115 }
116 }
117
118 if let Some(rule) = detect_auto_generation(field_name, field_schema) {
120 auto_generation.insert(field_name.clone(), rule);
121 }
122 }
123 ReferenceOr::Reference { reference } => {
124 let field_def =
127 FieldDefinition::new(field_name.clone(), "string".to_string()).optional();
128 fields.push(field_def);
129 }
130 }
131 }
132 } else {
133 return Err(Error::generic(format!(
134 "Schema '{}' is not an object type, cannot convert to entity",
135 schema_name
136 )));
137 }
138
139 if primary_key.is_empty() {
141 if fields.iter().any(|f| f.name == "id") {
143 primary_key.push("id".to_string());
144 auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
145 } else {
146 primary_key.push("id".to_string());
148 fields.insert(
149 0,
150 FieldDefinition::new("id".to_string(), "string".to_string())
151 .with_description("Auto-generated primary key".to_string()),
152 );
153 auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
154 }
155 }
156
157 let base_schema = SchemaDefinition {
159 name: schema_name.to_string(),
160 fields,
161 description: schema.schema_data.description.as_ref().map(|s| s.clone()),
162 metadata: HashMap::new(),
163 relationships: HashMap::new(),
164 };
165
166 let vbr_schema = VbrSchemaDefinition::new(base_schema).with_primary_key(primary_key);
168
169 let mut final_schema = vbr_schema;
171 for (field, rule) in auto_generation {
172 final_schema = final_schema.with_auto_generation(field, rule);
173 }
174
175 Ok(final_schema)
176}
177
178fn convert_field_to_definition(
180 field_name: &str,
181 schema: &Schema,
182 required_fields: &[String],
183) -> Result<FieldDefinition> {
184 let required = required_fields.contains(&field_name.to_string());
185 let field_type = schema_type_to_string(schema)?;
186 let description = schema.schema_data.description.clone();
187
188 let mut field_def = FieldDefinition::new(field_name.to_string(), field_type);
189
190 if !required {
191 field_def = field_def.optional();
192 }
193
194 if let Some(desc) = description {
195 field_def = field_def.with_description(desc);
196 }
197
198 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
200 if let Some(max_length) = string_type.max_length {
201 field_def = field_def.with_constraint("maxLength".to_string(), max_length.into());
202 }
203 if let Some(min_length) = string_type.min_length {
204 field_def = field_def.with_constraint("minLength".to_string(), min_length.into());
205 }
206 if let Some(pattern) = &string_type.pattern {
207 field_def = field_def.with_constraint("pattern".to_string(), pattern.clone().into());
208 }
209 } else if let SchemaKind::Type(Type::Integer(int_type)) = &schema.schema_kind {
210 if let Some(maximum) = int_type.maximum {
211 field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
212 }
213 if let Some(minimum) = int_type.minimum {
214 field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
215 }
216 } else if let SchemaKind::Type(Type::Number(num_type)) = &schema.schema_kind {
217 if let Some(maximum) = num_type.maximum {
218 field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
219 }
220 if let Some(minimum) = num_type.minimum {
221 field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
222 }
223 }
224
225 Ok(field_def)
226}
227
228fn schema_type_to_string(schema: &Schema) -> Result<String> {
230 match &schema.schema_kind {
231 SchemaKind::Type(Type::String(string_type)) => {
232 match &string_type.format {
234 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
235 openapiv3::StringFormat::Date => Ok("date".to_string()),
236 openapiv3::StringFormat::DateTime => Ok("datetime".to_string()),
237 _ => Ok("string".to_string()),
238 },
239 _ => Ok("string".to_string()),
240 }
241 }
242 SchemaKind::Type(Type::Integer(_)) => Ok("integer".to_string()),
243 SchemaKind::Type(Type::Number(_)) => Ok("number".to_string()),
244 SchemaKind::Type(Type::Boolean(_)) => Ok("boolean".to_string()),
245 SchemaKind::Type(Type::Array(_)) => Ok("array".to_string()),
246 SchemaKind::Type(Type::Object(_)) => Ok("object".to_string()),
247 _ => Ok("string".to_string()), }
249}
250
251fn is_primary_key_field(field_name: &str, field_def: &FieldDefinition) -> bool {
253 let pk_names = ["id", "uuid", "_id", "pk"];
255 if pk_names.contains(&field_name.to_lowercase().as_str()) {
256 return true;
257 }
258
259 if field_def.required {
261 false
263 } else {
264 false
265 }
266}
267
268fn detect_auto_generation(field_name: &str, schema: &Schema) -> Option<AutoGenerationRule> {
270 let name_lower = field_name.to_lowercase();
271
272 if name_lower.contains("uuid") || name_lower == "id" {
274 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
275 if let openapiv3::VariantOrUnknownOrEmpty::Item(_) = &string_type.format {
278 }
281 }
282 return Some(AutoGenerationRule::Uuid);
284 }
285
286 if name_lower.contains("timestamp")
288 || name_lower.contains("created_at")
289 || name_lower.contains("updated_at")
290 {
291 return Some(AutoGenerationRule::Timestamp);
292 }
293
294 if name_lower.contains("date") && !name_lower.contains("timestamp") {
296 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
297 if let openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Date) =
298 &string_type.format
299 {
300 return Some(AutoGenerationRule::Date);
301 }
302 }
303 }
304
305 None
306}
307
308fn detect_foreign_keys(
310 entity_name: &str,
311 vbr_schema: &mut VbrSchemaDefinition,
312 entity_names: &[String],
313 warnings: &mut Vec<String>,
314) {
315 for field in &vbr_schema.base.fields {
316 if is_foreign_key_field(&field.name, &entity_names) {
318 if let Some(target_entity) = extract_target_entity(&field.name, &entity_names) {
319 if !vbr_schema.foreign_keys.iter().any(|fk| fk.field == field.name) {
321 let fk = ForeignKeyDefinition {
322 field: field.name.clone(),
323 target_entity: target_entity.clone(),
324 target_field: "id".to_string(), on_delete: CascadeAction::NoAction,
326 on_update: CascadeAction::NoAction,
327 };
328 vbr_schema.foreign_keys.push(fk);
329 }
330 }
331 }
332 }
333}
334
335fn is_foreign_key_field(field_name: &str, entity_names: &[String]) -> bool {
337 let name_lower = field_name.to_lowercase();
338
339 if name_lower.ends_with("_id") {
341 return true;
342 }
343
344 for entity_name in entity_names {
346 let entity_lower = entity_name.to_lowercase();
347 if name_lower == entity_lower
349 || name_lower == format!("{}_id", entity_lower)
350 || name_lower == format!("{}id", entity_lower)
351 {
352 return true;
353 }
354 }
355
356 false
357}
358
359fn extract_target_entity(field_name: &str, entity_names: &[String]) -> Option<String> {
361 let name_lower = field_name.to_lowercase();
362
363 let base_name = name_lower.trim_end_matches("_id").trim_end_matches("id").to_string();
365
366 for entity_name in entity_names {
368 let entity_lower = entity_name.to_lowercase();
369 if base_name == entity_lower || name_lower == format!("{}_id", entity_lower) {
370 return Some(entity_name.clone());
371 }
372 }
373
374 None
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use mockforge_core::openapi::OpenApiSpec;
381
382 #[test]
383 fn test_extract_schemas() {
384 let spec_json = serde_json::json!({
385 "openapi": "3.0.0",
386 "info": {
387 "title": "Test API",
388 "version": "1.0.0"
389 },
390 "components": {
391 "schemas": {
392 "User": {
393 "type": "object",
394 "properties": {
395 "id": {
396 "type": "string",
397 "format": "uuid"
398 },
399 "name": {
400 "type": "string"
401 },
402 "email": {
403 "type": "string",
404 "format": "email"
405 }
406 },
407 "required": ["id", "name", "email"]
408 }
409 }
410 },
411 "paths": {}
412 });
413
414 let spec = OpenApiSpec::from_json(spec_json).unwrap();
415 let schemas = extract_schemas_from_openapi(&spec);
416
417 assert_eq!(schemas.len(), 1);
418 assert!(schemas.contains_key("User"));
419 }
420
421 #[test]
422 fn test_convert_schema_to_vbr() {
423 let spec_json = serde_json::json!({
424 "openapi": "3.0.0",
425 "info": {
426 "title": "Test API",
427 "version": "1.0.0"
428 },
429 "components": {
430 "schemas": {
431 "User": {
432 "type": "object",
433 "properties": {
434 "id": {
435 "type": "string",
436 "format": "uuid"
437 },
438 "name": {
439 "type": "string"
440 }
441 },
442 "required": ["id", "name"]
443 }
444 }
445 },
446 "paths": {}
447 });
448
449 let spec = OpenApiSpec::from_json(spec_json).unwrap();
450 let schemas = extract_schemas_from_openapi(&spec);
451 let user_schema = schemas.get("User").unwrap();
452
453 let result = convert_schema_to_vbr("User", user_schema.clone(), &schemas);
454 assert!(result.is_ok());
455
456 let vbr_schema = result.unwrap();
457 assert_eq!(vbr_schema.primary_key, vec!["id"]);
458 assert_eq!(vbr_schema.base.fields.len(), 2);
459 assert!(vbr_schema.auto_generation.contains_key("id"));
460 }
461}