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 match schema_ref {
80 ReferenceOr::Item(schema) => {
81 schemas.insert(name.clone(), schema.clone());
82 }
83 ReferenceOr::Reference { reference } => {
84 if let Some(resolved) = spec.get_schema(reference) {
86 schemas.insert(name.clone(), resolved.schema);
87 }
88 }
89 }
90 }
91 }
92 }
93
94 schemas
95}
96
97fn resolve_schema_reference(
102 reference: &str,
103 all_schemas: &HashMap<String, Schema>,
104) -> Option<Schema> {
105 let schema_name = reference.strip_prefix("#/components/schemas/")?;
107 all_schemas.get(schema_name).cloned()
108}
109
110fn convert_schema_to_vbr(
112 schema_name: &str,
113 schema: Schema,
114 all_schemas: &HashMap<String, Schema>,
115) -> Result<VbrSchemaDefinition> {
116 let mut fields = Vec::new();
117 let mut primary_key = Vec::new();
118 let mut auto_generation = HashMap::new();
119
120 if let SchemaKind::Type(Type::Object(obj_type)) = &schema.schema_kind {
122 for (field_name, field_schema_ref) in &obj_type.properties {
124 match field_schema_ref {
125 ReferenceOr::Item(field_schema) => {
126 let field_def =
127 convert_field_to_definition(field_name, field_schema, &obj_type.required)?;
128 fields.push(field_def.clone());
129
130 if is_primary_key_field(field_name, &field_def) {
132 primary_key.push(field_name.clone());
133 if primary_key.len() == 1 && !auto_generation.contains_key(field_name) {
135 auto_generation.insert(field_name.clone(), AutoGenerationRule::Uuid);
136 }
137 }
138
139 if let Some(rule) = detect_auto_generation(field_name, field_schema) {
141 auto_generation.insert(field_name.clone(), rule);
142 }
143 }
144 ReferenceOr::Reference { reference } => {
145 if let Some(resolved_schema) = resolve_schema_reference(reference, all_schemas)
147 {
148 match convert_field_to_definition(
150 field_name,
151 &resolved_schema,
152 &obj_type.required,
153 ) {
154 Ok(field_def) => {
155 fields.push(field_def.clone());
156
157 if is_primary_key_field(field_name, &field_def) {
159 primary_key.push(field_name.clone());
160 if primary_key.len() == 1
161 && !auto_generation.contains_key(field_name)
162 {
163 auto_generation
164 .insert(field_name.clone(), AutoGenerationRule::Uuid);
165 }
166 }
167
168 if let Some(rule) =
170 detect_auto_generation(field_name, &resolved_schema)
171 {
172 auto_generation.insert(field_name.clone(), rule);
173 }
174 }
175 Err(_e) => {
176 let field_def =
178 FieldDefinition::new(field_name.clone(), "string".to_string())
179 .optional();
180 fields.push(field_def);
181 }
182 }
183 } else {
184 let field_def =
186 FieldDefinition::new(field_name.clone(), "string".to_string())
187 .optional();
188 fields.push(field_def);
189 }
190 }
191 }
192 }
193 } else {
194 return Err(Error::generic(format!(
195 "Schema '{}' is not an object type, cannot convert to entity",
196 schema_name
197 )));
198 }
199
200 if primary_key.is_empty() {
202 if fields.iter().any(|f| f.name == "id") {
204 primary_key.push("id".to_string());
205 auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
206 } else {
207 primary_key.push("id".to_string());
209 fields.insert(
210 0,
211 FieldDefinition::new("id".to_string(), "string".to_string())
212 .with_description("Auto-generated primary key".to_string()),
213 );
214 auto_generation.insert("id".to_string(), AutoGenerationRule::Uuid);
215 }
216 }
217
218 let base_schema = SchemaDefinition {
220 name: schema_name.to_string(),
221 fields,
222 description: schema.schema_data.description.clone(),
223 metadata: HashMap::new(),
224 relationships: HashMap::new(),
225 };
226
227 let vbr_schema = VbrSchemaDefinition::new(base_schema).with_primary_key(primary_key);
229
230 let mut final_schema = vbr_schema;
232 for (field, rule) in auto_generation {
233 final_schema = final_schema.with_auto_generation(field, rule);
234 }
235
236 Ok(final_schema)
237}
238
239fn convert_field_to_definition(
241 field_name: &str,
242 schema: &Schema,
243 required_fields: &[String],
244) -> Result<FieldDefinition> {
245 let required = required_fields.contains(&field_name.to_string());
246 let field_type = schema_type_to_string(schema)?;
247 let description = schema.schema_data.description.clone();
248
249 let mut field_def = FieldDefinition::new(field_name.to_string(), field_type);
250
251 if !required {
252 field_def = field_def.optional();
253 }
254
255 if let Some(desc) = description {
256 field_def = field_def.with_description(desc);
257 }
258
259 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
261 if let Some(max_length) = string_type.max_length {
262 field_def = field_def.with_constraint("maxLength".to_string(), max_length.into());
263 }
264 if let Some(min_length) = string_type.min_length {
265 field_def = field_def.with_constraint("minLength".to_string(), min_length.into());
266 }
267 if let Some(pattern) = &string_type.pattern {
268 field_def = field_def.with_constraint("pattern".to_string(), pattern.clone().into());
269 }
270 } else if let SchemaKind::Type(Type::Integer(int_type)) = &schema.schema_kind {
271 if let Some(maximum) = int_type.maximum {
272 field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
273 }
274 if let Some(minimum) = int_type.minimum {
275 field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
276 }
277 } else if let SchemaKind::Type(Type::Number(num_type)) = &schema.schema_kind {
278 if let Some(maximum) = num_type.maximum {
279 field_def = field_def.with_constraint("maximum".to_string(), maximum.into());
280 }
281 if let Some(minimum) = num_type.minimum {
282 field_def = field_def.with_constraint("minimum".to_string(), minimum.into());
283 }
284 }
285
286 Ok(field_def)
287}
288
289fn schema_type_to_string(schema: &Schema) -> Result<String> {
291 match &schema.schema_kind {
292 SchemaKind::Type(Type::String(string_type)) => {
293 match &string_type.format {
295 openapiv3::VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
296 openapiv3::StringFormat::Date => Ok("date".to_string()),
297 openapiv3::StringFormat::DateTime => Ok("datetime".to_string()),
298 _ => Ok("string".to_string()),
299 },
300 _ => Ok("string".to_string()),
301 }
302 }
303 SchemaKind::Type(Type::Integer(_)) => Ok("integer".to_string()),
304 SchemaKind::Type(Type::Number(_)) => Ok("number".to_string()),
305 SchemaKind::Type(Type::Boolean(_)) => Ok("boolean".to_string()),
306 SchemaKind::Type(Type::Array(_)) => Ok("array".to_string()),
307 SchemaKind::Type(Type::Object(_)) => Ok("object".to_string()),
308 _ => Ok("string".to_string()), }
310}
311
312fn is_primary_key_field(field_name: &str, field_def: &FieldDefinition) -> bool {
314 let pk_names = ["id", "uuid", "_id", "pk"];
316 if pk_names.contains(&field_name.to_lowercase().as_str()) {
317 return true;
318 }
319
320 if field_def.required {
322 false
324 } else {
325 false
326 }
327}
328
329fn detect_auto_generation(field_name: &str, schema: &Schema) -> Option<AutoGenerationRule> {
331 let name_lower = field_name.to_lowercase();
332
333 if name_lower.contains("uuid") || name_lower == "id" {
335 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
336 if let openapiv3::VariantOrUnknownOrEmpty::Item(_) = &string_type.format {
339 }
342 }
343 return Some(AutoGenerationRule::Uuid);
345 }
346
347 if name_lower.contains("timestamp")
349 || name_lower.contains("created_at")
350 || name_lower.contains("updated_at")
351 {
352 return Some(AutoGenerationRule::Timestamp);
353 }
354
355 if name_lower.contains("date") && !name_lower.contains("timestamp") {
357 if let SchemaKind::Type(Type::String(string_type)) = &schema.schema_kind {
358 if let openapiv3::VariantOrUnknownOrEmpty::Item(openapiv3::StringFormat::Date) =
359 &string_type.format
360 {
361 return Some(AutoGenerationRule::Date);
362 }
363 }
364 }
365
366 None
367}
368
369fn detect_foreign_keys(
371 _entity_name: &str,
372 vbr_schema: &mut VbrSchemaDefinition,
373 entity_names: &[String],
374 _warnings: &mut [String],
375) {
376 for field in &vbr_schema.base.fields {
377 if is_foreign_key_field(&field.name, entity_names) {
379 if let Some(target_entity) = extract_target_entity(&field.name, entity_names) {
380 if !vbr_schema.foreign_keys.iter().any(|fk| fk.field == field.name) {
382 let fk = ForeignKeyDefinition {
383 field: field.name.clone(),
384 target_entity: target_entity.clone(),
385 target_field: "id".to_string(), on_delete: CascadeAction::NoAction,
387 on_update: CascadeAction::NoAction,
388 };
389 vbr_schema.foreign_keys.push(fk);
390 }
391 }
392 }
393 }
394}
395
396fn is_foreign_key_field(field_name: &str, entity_names: &[String]) -> bool {
398 let name_lower = field_name.to_lowercase();
399
400 if name_lower.ends_with("_id") {
402 return true;
403 }
404
405 for entity_name in entity_names {
407 let entity_lower = entity_name.to_lowercase();
408 if name_lower == entity_lower
410 || name_lower == format!("{}_id", entity_lower)
411 || name_lower == format!("{}id", entity_lower)
412 {
413 return true;
414 }
415 }
416
417 false
418}
419
420fn extract_target_entity(field_name: &str, entity_names: &[String]) -> Option<String> {
422 let name_lower = field_name.to_lowercase();
423
424 let base_name = name_lower.trim_end_matches("_id").trim_end_matches("id").to_string();
426
427 for entity_name in entity_names {
429 let entity_lower = entity_name.to_lowercase();
430 if base_name == entity_lower || name_lower == format!("{}_id", entity_lower) {
431 return Some(entity_name.clone());
432 }
433 }
434
435 None
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use mockforge_core::openapi::OpenApiSpec;
442
443 #[test]
444 fn test_extract_schemas() {
445 let spec_json = serde_json::json!({
446 "openapi": "3.0.0",
447 "info": {
448 "title": "Test API",
449 "version": "1.0.0"
450 },
451 "components": {
452 "schemas": {
453 "User": {
454 "type": "object",
455 "properties": {
456 "id": {
457 "type": "string",
458 "format": "uuid"
459 },
460 "name": {
461 "type": "string"
462 },
463 "email": {
464 "type": "string",
465 "format": "email"
466 }
467 },
468 "required": ["id", "name", "email"]
469 }
470 }
471 },
472 "paths": {}
473 });
474
475 let spec = OpenApiSpec::from_json(spec_json).unwrap();
476 let schemas = extract_schemas_from_openapi(&spec);
477
478 assert_eq!(schemas.len(), 1);
479 assert!(schemas.contains_key("User"));
480 }
481
482 #[test]
483 fn test_convert_schema_to_vbr() {
484 let spec_json = serde_json::json!({
485 "openapi": "3.0.0",
486 "info": {
487 "title": "Test API",
488 "version": "1.0.0"
489 },
490 "components": {
491 "schemas": {
492 "User": {
493 "type": "object",
494 "properties": {
495 "id": {
496 "type": "string",
497 "format": "uuid"
498 },
499 "name": {
500 "type": "string"
501 }
502 },
503 "required": ["id", "name"]
504 }
505 }
506 },
507 "paths": {}
508 });
509
510 let spec = OpenApiSpec::from_json(spec_json).unwrap();
511 let schemas = extract_schemas_from_openapi(&spec);
512 let user_schema = schemas.get("User").unwrap();
513
514 let result = convert_schema_to_vbr("User", user_schema.clone(), &schemas);
515 assert!(result.is_ok());
516
517 let vbr_schema = result.unwrap();
518 assert_eq!(vbr_schema.primary_key, vec!["id"]);
519 assert_eq!(vbr_schema.base.fields.len(), 2);
520 assert!(vbr_schema.auto_generation.contains_key("id"));
521 }
522}