Skip to main content

mockforge_import/import/
openapi_import.rs

1//! OpenAPI specification import functionality
2//!
3//! This module handles parsing OpenAPI/Swagger specifications and converting them
4//! to MockForge routes and configurations.
5
6use crate::import::schema_data_generator::generate_from_schema;
7use mockforge_openapi::OpenApiSpec;
8
9use once_cell::sync::Lazy;
10use regex::Regex;
11use serde::Serialize;
12use serde_json::{json, Value};
13use std::collections::HashMap;
14
15// Pre-compiled regex for path parameter conversion
16static PATH_PARAM_RE: Lazy<Regex> =
17    Lazy::new(|| Regex::new(r"\{([^}]+)\}").expect("PATH_PARAM_RE regex is valid"));
18
19/// Result of importing an OpenAPI specification
20#[derive(Debug)]
21pub struct OpenApiImportResult {
22    /// Converted routes from OpenAPI paths/operations
23    pub routes: Vec<MockForgeRoute>,
24    /// Warnings encountered during import
25    pub warnings: Vec<String>,
26    /// Extracted specification metadata
27    pub spec_info: OpenApiSpecInfo,
28}
29
30/// MockForge route structure for OpenAPI import
31#[derive(Debug, Serialize)]
32pub struct MockForgeRoute {
33    /// HTTP method
34    pub method: String,
35    /// Request path (with Express-style path parameters)
36    pub path: String,
37    /// Request headers
38    pub headers: HashMap<String, String>,
39    /// Optional request body
40    pub body: Option<String>,
41    /// Mock response for this route
42    pub response: MockForgeResponse,
43}
44
45/// MockForge response structure
46#[derive(Debug, Serialize)]
47pub struct MockForgeResponse {
48    /// HTTP status code
49    pub status: u16,
50    /// Response headers
51    pub headers: HashMap<String, String>,
52    /// Response body
53    pub body: Value,
54}
55
56/// OpenAPI specification metadata
57#[derive(Debug)]
58pub struct OpenApiSpecInfo {
59    /// API title
60    pub title: String,
61    /// API version
62    pub version: String,
63    /// Optional API description
64    pub description: Option<String>,
65    /// OpenAPI specification version (e.g., "3.0.3")
66    pub openapi_version: String,
67    /// List of server URLs from the spec
68    pub servers: Vec<String>,
69}
70
71/// Import an OpenAPI specification
72pub fn import_openapi_spec(
73    content: &str,
74    _base_url: Option<&str>,
75) -> Result<OpenApiImportResult, String> {
76    // Detect format and validate using enhanced validator
77    let format = mockforge_openapi::spec_parser::SpecFormat::detect(content, None)
78        .map_err(|e| format!("Failed to detect spec format: {}", e))?;
79
80    // Parse as JSON value first for validation - optimized to avoid double parsing
81    // Try JSON first, then YAML (more robust detection)
82    let mut json_value: Value = match serde_json::from_str::<Value>(content) {
83        Ok(val) => val,
84        Err(_) => {
85            // Try YAML if JSON parsing fails
86            serde_yaml::from_str(content)
87                .map_err(|e| format!("Failed to parse as JSON or YAML: {}", e))?
88        }
89    };
90
91    // Validate using enhanced validator for better error messages
92    match format {
93        mockforge_openapi::spec_parser::SpecFormat::OpenApi20 => {
94            let validation =
95                mockforge_openapi::spec_parser::OpenApiValidator::validate(&json_value, format);
96            if !validation.is_valid {
97                // Format errors on separate lines for better readability
98                let error_msg = validation
99                    .errors
100                    .iter()
101                    .map(|e| format!("  - {}", e))
102                    .collect::<Vec<_>>()
103                    .join("\n");
104                return Err(format!("Invalid OpenAPI 2.0 (Swagger) specification:\n{}", error_msg));
105            }
106
107            // #838 — up-convert Swagger 2.0 → OpenAPI 3.0 and fall
108            // through to the normal parse path, so legacy specs import
109            // without an external converter step.
110            json_value = super::swagger2_convert::convert_swagger2_to_openapi3(&json_value)?;
111        }
112        mockforge_openapi::spec_parser::SpecFormat::OpenApi30
113        | mockforge_openapi::spec_parser::SpecFormat::OpenApi31 => {
114            let validation =
115                mockforge_openapi::spec_parser::OpenApiValidator::validate(&json_value, format);
116            if !validation.is_valid {
117                // Format errors on separate lines for better readability
118                let error_msg = validation
119                    .errors
120                    .iter()
121                    .map(|e| format!("  - {}", e))
122                    .collect::<Vec<_>>()
123                    .join("\n");
124                return Err(format!("Invalid OpenAPI specification:\n{}", error_msg));
125            }
126            // Continue with parsing
127        }
128        _ => {
129            return Err(format!(
130                "Unsupported specification format: {}. Only OpenAPI 3.x is currently supported for parsing.",
131                format.display_name()
132            ));
133        }
134    }
135
136    let spec = OpenApiSpec::from_json(json_value)
137        .map_err(|e| format!("Failed to load OpenAPI spec: {}", e))?;
138
139    spec.validate().map_err(|e| format!("Invalid OpenAPI specification: {}", e))?;
140
141    // Extract spec info
142    let spec_info = OpenApiSpecInfo {
143        title: spec.title().to_string(),
144        version: spec.api_version().to_string(),
145        description: spec.description().map(|s| s.to_string()),
146        openapi_version: spec.version().to_string(),
147        servers: spec
148            .spec
149            .servers
150            .iter()
151            .filter_map(|server| server.url.parse::<url::Url>().ok())
152            .map(|url| url.to_string())
153            .collect(),
154    };
155
156    let mut routes = Vec::new();
157    let mut warnings = Vec::new();
158
159    // Process all paths and operations in deterministic order
160    let path_operations = spec.all_paths_and_operations();
161
162    // Sort paths alphabetically for deterministic ordering
163    let mut sorted_paths: Vec<_> = path_operations.iter().collect();
164    sorted_paths.sort_by_key(|(path, _)| path.as_str());
165
166    for (path, operations) in sorted_paths {
167        // Process operations in a specific order for deterministic results
168        let method_order = [
169            "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE",
170        ];
171
172        for method in method_order {
173            if let Some(operation) = operations.get(method) {
174                match convert_operation_to_route(&spec, method, path, operation, _base_url) {
175                    Ok(route) => routes.push(route),
176                    Err(e) => warnings.push(format!("Failed to convert {method} {path}: {e}")),
177                }
178            }
179        }
180    }
181
182    Ok(OpenApiImportResult {
183        routes,
184        warnings,
185        spec_info,
186    })
187}
188
189/// Convert an OpenAPI operation to a MockForge route
190fn convert_operation_to_route(
191    spec: &OpenApiSpec,
192    method: &str,
193    path: &str,
194    operation: &openapiv3::Operation,
195    _base_url: Option<&str>,
196) -> Result<MockForgeRoute, String> {
197    // Use the first 200-series response as the default response
198    let mut response_status = 200;
199    let mut response_body = Value::Object(serde_json::Map::new());
200    let mut response_headers = HashMap::new();
201
202    // Find the first success response (200-299)
203    for (status_code, response_ref) in &operation.responses.responses {
204        // Handle different StatusCode types
205        let is_success = match status_code {
206            openapiv3::StatusCode::Code(code) => (200..300).contains(code),
207            openapiv3::StatusCode::Range(range) => *range == 2, // 2XX means success
208        };
209
210        if is_success {
211            let status = match status_code {
212                openapiv3::StatusCode::Code(code) => *code,
213                openapiv3::StatusCode::Range(_) => 200, // Default to 200 for 2XX
214            };
215
216            if (200..300).contains(&status) {
217                response_status = status;
218
219                // Try to resolve the response and extract content
220                if let Some(response) = response_ref.as_item() {
221                    // Add default content-type header
222                    response_headers
223                        .insert("Content-Type".to_string(), "application/json".to_string());
224
225                    // Try to generate a sample response from schema
226                    if let Some(content) = response.content.get("application/json") {
227                        // Check for examples first
228                        if let Some(example) = &content.example {
229                            response_body = example.clone();
230                        } else if !content.examples.is_empty() {
231                            // Use the first example
232                            if let Some((_key, example_ref)) = content.examples.iter().next() {
233                                if let Some(example_value) = example_ref.as_item() {
234                                    if let Some(value) = &example_value.value {
235                                        response_body = value.clone();
236                                    }
237                                }
238                            }
239                        } else if let Some(schema_ref) = &content.schema {
240                            // Generate from schema, resolving $ref if needed
241                            response_body = if let Some(resolved) =
242                                resolve_schema_ref(schema_ref, &spec.spec)
243                            {
244                                generate_response_from_openapi_schema(&resolved)
245                            } else {
246                                serde_json::json!({"message": "Mock response", "path": path, "method": method})
247                            };
248                        } else {
249                            // No schema or example, basic response
250                            response_body = serde_json::json!({"message": "Success"});
251                        }
252                    } else {
253                        // No content schema, provide a basic response
254                        response_body = serde_json::json!({"message": "Success"});
255                    }
256                } else {
257                    // Default response if reference can't be resolved
258                    response_body = serde_json::json!({"message": "Mock response"});
259                }
260                break;
261            }
262        }
263    }
264
265    // Check for default response if no success response found
266    if response_status == 200 && operation.responses.default.is_some() {
267        response_body = serde_json::json!({"message": "Default response"});
268    }
269
270    let mock_response = MockForgeResponse {
271        status: response_status,
272        headers: response_headers,
273        body: response_body,
274    };
275
276    // Convert OpenAPI path parameters {param} to Express-style :param
277    let converted_path = convert_path_parameters(path);
278
279    // Extract request body if present
280    let request_body = if let Some(request_body_ref) = &operation.request_body {
281        extract_request_body_example(request_body_ref, &spec.spec)
282    } else {
283        None
284    };
285
286    Ok(MockForgeRoute {
287        method: method.to_uppercase(),
288        path: converted_path,
289        headers: HashMap::new(), // Could extract from parameters in a full implementation
290        body: request_body,
291        response: mock_response,
292    })
293}
294
295/// Extract request body example from OpenAPI request body reference
296fn extract_request_body_example(
297    request_body_ref: &openapiv3::ReferenceOr<openapiv3::RequestBody>,
298    spec: &openapiv3::OpenAPI,
299) -> Option<String> {
300    let request_body = match request_body_ref {
301        openapiv3::ReferenceOr::Item(rb) => rb.clone(),
302        openapiv3::ReferenceOr::Reference { reference } => {
303            // Resolve $ref like "#/components/requestBodies/MyBody"
304            let name = reference.strip_prefix("#/components/requestBodies/")?;
305            let components = spec.components.as_ref()?;
306            let rb_ref = components.request_bodies.get(name)?;
307            match rb_ref {
308                openapiv3::ReferenceOr::Item(rb) => rb.clone(),
309                openapiv3::ReferenceOr::Reference { .. } => return None,
310            }
311        }
312    };
313
314    // Look for application/json content type
315    let media_type = request_body.content.get("application/json")?;
316
317    // Check if there's an explicit example
318    if let Some(example) = &media_type.example {
319        if let Ok(example_str) = serde_json::to_string(example) {
320            return Some(example_str);
321        }
322    }
323
324    // Generate mock data from schema
325    if let Some(schema_ref) = &media_type.schema {
326        let schema = resolve_schema_ref(schema_ref, spec);
327        if let Some(s) = schema {
328            let json_schema = openapi_schema_to_json_schema(&s);
329            let generated = generate_from_schema(&json_schema);
330            if let Ok(s) = serde_json::to_string(&generated) {
331                return Some(s);
332            }
333        }
334    }
335
336    None
337}
338
339/// Resolve a schema reference to an owned Schema
340fn resolve_schema_ref(
341    schema_ref: &openapiv3::ReferenceOr<openapiv3::Schema>,
342    spec: &openapiv3::OpenAPI,
343) -> Option<openapiv3::Schema> {
344    match schema_ref {
345        openapiv3::ReferenceOr::Item(schema) => Some(schema.clone()),
346        openapiv3::ReferenceOr::Reference { reference } => {
347            let name = reference.strip_prefix("#/components/schemas/")?;
348            let components = spec.components.as_ref()?;
349            let resolved = components.schemas.get(name)?;
350            match resolved {
351                openapiv3::ReferenceOr::Item(schema) => Some(schema.clone()),
352                openapiv3::ReferenceOr::Reference { .. } => None,
353            }
354        }
355    }
356}
357
358/// Convert OpenAPI path parameters {param} to Express-style :param
359fn convert_path_parameters(path: &str) -> String {
360    PATH_PARAM_RE.replace_all(path, ":$1").to_string()
361}
362
363/// Generate response from OpenAPI schema
364fn generate_response_from_openapi_schema(schema: &openapiv3::Schema) -> Value {
365    // Convert OpenAPI schema to JSON Schema format for our generator
366    let json_schema = openapi_schema_to_json_schema(schema);
367    generate_from_schema(&json_schema)
368}
369
370/// Convert OpenAPI Schema to JSON Schema Value
371fn openapi_schema_to_json_schema(schema: &openapiv3::Schema) -> Value {
372    match &schema.schema_kind {
373        openapiv3::SchemaKind::Type(type_schema) => match type_schema {
374            openapiv3::Type::String(string_type) => {
375                let mut obj = serde_json::Map::new();
376                obj.insert("type".to_string(), json!("string"));
377
378                // Format is VariantOrUnknownOrEmpty, check if it has a value
379                if !matches!(string_type.format, openapiv3::VariantOrUnknownOrEmpty::Empty) {
380                    obj.insert("format".to_string(), json!(format!("{:?}", string_type.format)));
381                }
382
383                // enumeration is Vec<Option<String>>, not Option
384                if !string_type.enumeration.is_empty() {
385                    let enum_values: Vec<Value> = string_type
386                        .enumeration
387                        .iter()
388                        .filter_map(|s| s.as_ref().map(|s| json!(s)))
389                        .collect();
390                    if !enum_values.is_empty() {
391                        obj.insert("enum".to_string(), json!(enum_values));
392                    }
393                }
394
395                Value::Object(obj)
396            }
397            openapiv3::Type::Number(_) => {
398                json!({"type": "number"})
399            }
400            openapiv3::Type::Integer(_) => {
401                json!({"type": "integer"})
402            }
403            openapiv3::Type::Boolean(_) => {
404                json!({"type": "boolean"})
405            }
406            openapiv3::Type::Array(array_type) => {
407                let mut obj = serde_json::Map::new();
408                obj.insert("type".to_string(), json!("array"));
409
410                if let Some(items) = &array_type.items {
411                    if let Some(item_schema) = items.as_item() {
412                        obj.insert("items".to_string(), openapi_schema_to_json_schema(item_schema));
413                    }
414                }
415
416                Value::Object(obj)
417            }
418            openapiv3::Type::Object(object_type) => {
419                let mut obj = serde_json::Map::new();
420                obj.insert("type".to_string(), json!("object"));
421
422                if !object_type.properties.is_empty() {
423                    let mut props = serde_json::Map::new();
424                    for (name, schema_ref) in &object_type.properties {
425                        if let Some(prop_schema) = schema_ref.as_item() {
426                            props.insert(name.clone(), openapi_schema_to_json_schema(prop_schema));
427                        }
428                    }
429                    obj.insert("properties".to_string(), Value::Object(props));
430                }
431
432                if !object_type.required.is_empty() {
433                    obj.insert("required".to_string(), json!(object_type.required));
434                }
435
436                Value::Object(obj)
437            }
438        },
439        openapiv3::SchemaKind::OneOf { one_of } => {
440            // Use the first variant for mock data generation
441            if let Some(first) = one_of.first() {
442                if let Some(schema) = first.as_item() {
443                    return openapi_schema_to_json_schema(schema);
444                }
445            }
446            json!({"type": "object"})
447        }
448        openapiv3::SchemaKind::AllOf { all_of } => {
449            // Merge all schemas into a single object with combined properties
450            let mut properties = serde_json::Map::new();
451            let mut required = Vec::new();
452            for schema_ref in all_of {
453                if let Some(sub_schema) = schema_ref.as_item() {
454                    let converted = openapi_schema_to_json_schema(sub_schema);
455                    if let Some(obj) = converted.as_object() {
456                        if let Some(props) = obj.get("properties").and_then(|p| p.as_object()) {
457                            for (k, v) in props {
458                                properties.insert(k.clone(), v.clone());
459                            }
460                        }
461                        if let Some(req) = obj.get("required").and_then(|r| r.as_array()) {
462                            for r in req {
463                                if let Some(s) = r.as_str() {
464                                    required.push(json!(s));
465                                }
466                            }
467                        }
468                    }
469                }
470            }
471            let mut result = serde_json::Map::new();
472            result.insert("type".to_string(), json!("object"));
473            if !properties.is_empty() {
474                result.insert("properties".to_string(), Value::Object(properties));
475            }
476            if !required.is_empty() {
477                result.insert("required".to_string(), Value::Array(required));
478            }
479            Value::Object(result)
480        }
481        openapiv3::SchemaKind::AnyOf { any_of } => {
482            // Use the first variant for mock data generation
483            if let Some(first) = any_of.first() {
484                if let Some(schema) = first.as_item() {
485                    return openapi_schema_to_json_schema(schema);
486                }
487            }
488            json!({"type": "object"})
489        }
490        openapiv3::SchemaKind::Not { .. } => {
491            json!({"type": "object"})
492        }
493        openapiv3::SchemaKind::Any(_) => {
494            json!({"type": "object"})
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_import_openapi_spec() {
505        let openapi_json = r#"{
506            "openapi": "3.0.3",
507            "info": {
508                "title": "Test API",
509                "version": "1.0.0",
510                "description": "A test API"
511            },
512            "paths": {
513                "/users": {
514                    "get": {
515                        "operationId": "getUsers",
516                        "summary": "Get all users",
517                        "responses": {
518                            "200": {
519                                "description": "Successful response",
520                                "content": {
521                                    "application/json": {
522                                        "schema": {
523                                            "type": "array",
524                                            "items": {
525                                                "type": "object",
526                                                "properties": {
527                                                    "id": {"type": "integer"},
528                                                    "name": {"type": "string"}
529                                                }
530                                            }
531                                        }
532                                    }
533                                }
534                            }
535                        }
536                    }
537                }
538            }
539        }"#;
540
541        let result = import_openapi_spec(openapi_json, Some("/api")).unwrap();
542
543        assert_eq!(result.routes.len(), 1);
544        assert_eq!(result.routes[0].method, "GET");
545        assert_eq!(result.routes[0].path, "/users");
546        assert_eq!(result.routes[0].response.status, 200);
547
548        // Check spec info
549        assert_eq!(result.spec_info.title, "Test API");
550        assert_eq!(result.spec_info.version, "1.0.0");
551    }
552
553    #[test]
554    fn test_import_openapi_with_parameters() {
555        let openapi_json = r#"{
556            "openapi": "3.0.3",
557            "info": {
558                "title": "Test API",
559                "version": "1.0.0"
560            },
561            "paths": {
562                "/users/{userId}": {
563                    "get": {
564                        "operationId": "getUser",
565                        "parameters": [
566                            {
567                                "name": "userId",
568                                "in": "path",
569                                "required": true,
570                                "schema": {"type": "string"}
571                            }
572                        ],
573                        "responses": {
574                            "200": {
575                                "description": "User info",
576                                "content": {
577                                    "application/json": {
578                                        "schema": {
579                                            "type": "object",
580                                            "properties": {
581                                                "id": {"type": "string"},
582                                                "name": {"type": "string"}
583                                            }
584                                        }
585                                    }
586                                }
587                            }
588                        }
589                    }
590                }
591            }
592        }"#;
593
594        let result = import_openapi_spec(openapi_json, None).unwrap();
595
596        assert_eq!(result.routes.len(), 1);
597        assert_eq!(result.routes[0].path, "/users/:userId");
598    }
599
600    #[test]
601    fn test_import_openapi_with_multiple_operations() {
602        let openapi_json = r#"{
603            "openapi": "3.0.3",
604            "info": {
605                "title": "User API",
606                "version": "1.0.0"
607            },
608            "paths": {
609                "/users": {
610                    "get": {
611                        "operationId": "listUsers",
612                        "responses": {
613                            "200": {
614                                "description": "List of users",
615                                "content": {
616                                    "application/json": {
617                                        "schema": {
618                                            "type": "array",
619                                            "items": {"type": "object"}
620                                        }
621                                    }
622                                }
623                            }
624                        }
625                    },
626                    "post": {
627                        "operationId": "createUser",
628                        "requestBody": {
629                            "required": true,
630                            "content": {
631                                "application/json": {
632                                    "schema": {
633                                        "type": "object",
634                                        "properties": {
635                                            "name": {"type": "string"},
636                                            "email": {"type": "string"}
637                                        }
638                                    }
639                                }
640                            }
641                        },
642                        "responses": {
643                            "201": {
644                                "description": "User created",
645                                "content": {
646                                    "application/json": {
647                                        "schema": {"type": "object"}
648                                    }
649                                }
650                            }
651                        }
652                    }
653                },
654                "/users/{id}": {
655                    "get": {
656                        "operationId": "getUser",
657                        "parameters": [
658                            {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}
659                        ],
660                        "responses": {
661                            "200": {
662                                "description": "User details",
663                                "content": {
664                                    "application/json": {
665                                        "schema": {"type": "object"}
666                                    }
667                                }
668                            }
669                        }
670                    },
671                    "put": {
672                        "operationId": "updateUser",
673                        "parameters": [
674                            {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}
675                        ],
676                        "requestBody": {
677                            "required": true,
678                            "content": {
679                                "application/json": {
680                                    "schema": {"type": "object"}
681                                }
682                            }
683                        },
684                        "responses": {
685                            "200": {
686                                "description": "User updated",
687                                "content": {
688                                    "application/json": {
689                                        "schema": {"type": "object"}
690                                    }
691                                }
692                            }
693                        }
694                    },
695                    "delete": {
696                        "operationId": "deleteUser",
697                        "parameters": [
698                            {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}
699                        ],
700                        "responses": {
701                            "204": {
702                                "description": "User deleted"
703                            }
704                        }
705                    }
706                }
707            }
708        }"#;
709
710        let result = import_openapi_spec(openapi_json, None).unwrap();
711
712        assert_eq!(result.routes.len(), 5);
713
714        // Check each route
715        assert_eq!(result.routes[0].method, "GET");
716        assert_eq!(result.routes[0].path, "/users");
717        assert_eq!(result.routes[0].response.status, 200);
718
719        assert_eq!(result.routes[1].method, "POST");
720        assert_eq!(result.routes[1].path, "/users");
721        assert_eq!(result.routes[1].response.status, 201);
722
723        assert_eq!(result.routes[2].method, "GET");
724        assert_eq!(result.routes[2].path, "/users/:id");
725
726        assert_eq!(result.routes[3].method, "PUT");
727        assert_eq!(result.routes[3].path, "/users/:id");
728        assert_eq!(result.routes[3].response.status, 200);
729
730        assert_eq!(result.routes[4].method, "DELETE");
731        assert_eq!(result.routes[4].path, "/users/:id");
732        assert_eq!(result.routes[4].response.status, 204);
733    }
734
735    #[test]
736    fn test_import_openapi_with_query_parameters() {
737        let openapi_json = r#"{
738            "openapi": "3.0.3",
739            "info": {
740                "title": "Search API",
741                "version": "1.0.0"
742            },
743            "paths": {
744                "/search": {
745                    "get": {
746                        "operationId": "searchUsers",
747                        "parameters": [
748                            {"name": "query", "in": "query", "required": true, "schema": {"type": "string"}},
749                            {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer", "default": 10}},
750                            {"name": "offset", "in": "query", "required": false, "schema": {"type": "integer", "default": 0}}
751                        ],
752                        "responses": {
753                            "200": {
754                                "description": "Search results",
755                                "content": {
756                                    "application/json": {
757                                        "schema": {"type": "object"}
758                                    }
759                                }
760                            }
761                        }
762                    }
763                }
764            }
765        }"#;
766
767        let result = import_openapi_spec(openapi_json, None).unwrap();
768
769        assert_eq!(result.routes.len(), 1);
770        assert_eq!(result.routes[0].method, "GET");
771        assert_eq!(result.routes[0].path, "/search");
772    }
773
774    #[test]
775    fn test_import_openapi_with_request_body() {
776        let openapi_json = r#"{
777            "openapi": "3.0.3",
778            "info": {
779                "title": "User API",
780                "version": "1.0.0"
781            },
782            "paths": {
783                "/users": {
784                    "post": {
785                        "operationId": "createUser",
786                        "requestBody": {
787                            "required": true,
788                            "content": {
789                                "application/json": {
790                                    "schema": {
791                                        "type": "object",
792                                        "properties": {
793                                            "name": {"type": "string"},
794                                            "email": {"type": "string"},
795                                            "age": {"type": "integer"}
796                                        },
797                                        "required": ["name", "email"]
798                                    },
799                                    "example": {
800                                        "name": "John Doe",
801                                        "email": "john@example.com",
802                                        "age": 30
803                                    }
804                                }
805                            }
806                        },
807                        "responses": {
808                            "201": {
809                                "description": "User created",
810                                "content": {
811                                    "application/json": {
812                                        "schema": {"type": "object"}
813                                    }
814                                }
815                            }
816                        }
817                    }
818                }
819            }
820        }"#;
821
822        let result = import_openapi_spec(openapi_json, None).unwrap();
823
824        assert_eq!(result.routes.len(), 1);
825        assert_eq!(result.routes[0].method, "POST");
826        assert_eq!(result.routes[0].path, "/users");
827        assert_eq!(result.routes[0].response.status, 201);
828        assert!(result.routes[0].body.is_some());
829    }
830
831    #[test]
832    fn test_import_openapi_with_different_response_codes() {
833        let openapi_json = r#"{
834            "openapi": "3.0.3",
835            "info": {
836                "title": "Test API",
837                "version": "1.0.0"
838            },
839            "paths": {
840                "/users": {
841                    "get": {
842                        "responses": {
843                            "200": {"description": "Success"},
844                            "400": {"description": "Bad Request"},
845                            "404": {"description": "Not Found"},
846                            "500": {"description": "Internal Error"}
847                        }
848                    }
849                }
850            }
851        }"#;
852
853        let result = import_openapi_spec(openapi_json, None).unwrap();
854
855        assert_eq!(result.routes.len(), 1);
856        assert_eq!(result.routes[0].method, "GET");
857        assert_eq!(result.routes[0].path, "/users");
858        // Should pick the first 2xx response (200)
859        assert_eq!(result.routes[0].response.status, 200);
860    }
861
862    #[test]
863    fn test_import_openapi_with_default_response() {
864        let openapi_json = r#"{
865            "openapi": "3.0.3",
866            "info": {
867                "title": "Test API",
868                "version": "1.0.0"
869            },
870            "paths": {
871                "/users": {
872                    "get": {
873                        "responses": {
874                            "default": {
875                                "description": "Default response",
876                                "content": {
877                                    "application/json": {
878                                        "schema": {"type": "object"}
879                                    }
880                                }
881                            }
882                        }
883                    }
884                }
885            }
886        }"#;
887
888        let result = import_openapi_spec(openapi_json, None).unwrap();
889
890        assert_eq!(result.routes.len(), 1);
891        assert_eq!(result.routes[0].method, "GET");
892        assert_eq!(result.routes[0].path, "/users");
893        assert_eq!(result.routes[0].response.status, 200); // Default should use 200
894    }
895
896    #[test]
897    fn test_import_openapi_with_schema_references() {
898        let openapi_json = r##"{
899            "openapi": "3.0.3",
900            "info": {
901                "title": "Test API",
902                "version": "1.0.0"
903            },
904            "components": {
905                "schemas": {
906                    "User": {
907                        "type": "object",
908                        "properties": {
909                            "id": {"type": "integer"},
910                            "name": {"type": "string"},
911                            "email": {"type": "string"}
912                        }
913                    },
914                    "Error": {
915                        "type": "object",
916                        "properties": {
917                            "code": {"type": "integer"},
918                            "message": {"type": "string"}
919                        }
920                    }
921                }
922            },
923            "paths": {
924                "/users": {
925                    "get": {
926                        "responses": {
927                            "200": {
928                                "description": "Success",
929                                "content": {
930                                    "application/json": {
931                                        "schema": {"$ref": "#components/schemas/User"}
932                                    }
933                                }
934                            }
935                        }
936                    }
937                }
938            }
939        }"##;
940
941        let result = import_openapi_spec(openapi_json, None).unwrap();
942
943        assert_eq!(result.routes.len(), 1);
944        assert_eq!(result.routes[0].method, "GET");
945        assert_eq!(result.routes[0].path, "/users");
946        assert_eq!(result.routes[0].response.status, 200);
947    }
948
949    #[test]
950    fn test_import_openapi_with_array_responses() {
951        let openapi_json = r#"{
952            "openapi": "3.0.3",
953            "info": {
954                "title": "Test API",
955                "version": "1.0.0"
956            },
957            "paths": {
958                "/users": {
959                    "get": {
960                        "responses": {
961                            "200": {
962                                "description": "List of users",
963                                "content": {
964                                    "application/json": {
965                                        "schema": {
966                                            "type": "array",
967                                            "items": {
968                                                "type": "object",
969                                                "properties": {
970                                                    "id": {"type": "integer"},
971                                                    "name": {"type": "string"}
972                                                }
973                                            }
974                                        }
975                                    }
976                                }
977                            }
978                        }
979                    }
980                }
981            }
982        }"#;
983
984        let result = import_openapi_spec(openapi_json, None).unwrap();
985
986        assert_eq!(result.routes.len(), 1);
987        assert_eq!(result.routes[0].method, "GET");
988        assert_eq!(result.routes[0].path, "/users");
989        assert_eq!(result.routes[0].response.status, 200);
990    }
991
992    #[test]
993    fn test_import_openapi_with_complex_schema() {
994        let openapi_json = r#"{
995            "openapi": "3.0.3",
996            "info": {
997                "title": "Complex API",
998                "version": "1.0.0"
999            },
1000            "paths": {
1001                "/users/{userId}/posts": {
1002                    "get": {
1003                        "parameters": [
1004                            {"name": "userId", "in": "path", "required": true, "schema": {"type": "string"}},
1005                            {"name": "includeComments", "in": "query", "required": false, "schema": {"type": "boolean"}},
1006                            {"name": "limit", "in": "query", "required": false, "schema": {"type": "integer", "default": 10}}
1007                        ],
1008                        "responses": {
1009                            "200": {
1010                                "description": "User posts",
1011                                "content": {
1012                                    "application/json": {
1013                                        "schema": {
1014                                            "type": "object",
1015                                            "properties": {
1016                                                "posts": {
1017                                                    "type": "array",
1018                                                    "items": {
1019                                                        "type": "object",
1020                                                        "properties": {
1021                                                            "id": {"type": "integer"},
1022                                                            "title": {"type": "string"},
1023                                                            "content": {"type": "string"},
1024                                                            "author": {
1025                                                                "type": "object",
1026                                                                "properties": {
1027                                                                    "id": {"type": "integer"},
1028                                                                    "name": {"type": "string"}
1029                                                                }
1030                                                            },
1031                                                            "tags": {
1032                                                                "type": "array",
1033                                                                "items": {"type": "string"}
1034                                                            }
1035                                                        }
1036                                                    }
1037                                                },
1038                                                "total": {"type": "integer"},
1039                                                "page": {"type": "integer"}
1040                                            }
1041                                        }
1042                                    }
1043                                }
1044                            }
1045                        }
1046                    }
1047                }
1048            }
1049        }"#;
1050
1051        let result = import_openapi_spec(openapi_json, None).unwrap();
1052
1053        assert_eq!(result.routes.len(), 1);
1054        assert_eq!(result.routes[0].method, "GET");
1055        assert_eq!(result.routes[0].path, "/users/:userId/posts");
1056        assert_eq!(result.routes[0].response.status, 200);
1057    }
1058
1059    #[test]
1060    fn test_import_openapi_with_base_url() {
1061        let openapi_json = r#"{
1062            "openapi": "3.0.3",
1063            "info": {
1064                "title": "Test API",
1065                "version": "1.0.0"
1066            },
1067            "servers": [
1068                {"url": "https://api.example.com/v1"},
1069                {"url": "https://dev.example.com/v1"}
1070            ],
1071            "paths": {
1072                "/users": {
1073                    "get": {
1074                        "responses": {
1075                            "200": {
1076                                "description": "Success",
1077                                "content": {
1078                                    "application/json": {
1079                                        "schema": {"type": "object"}
1080                                    }
1081                                }
1082                            }
1083                        }
1084                    }
1085                }
1086            }
1087        }"#;
1088
1089        let result = import_openapi_spec(openapi_json, Some("https://api.example.com/v1")).unwrap();
1090
1091        assert_eq!(result.routes.len(), 1);
1092        assert_eq!(result.routes[0].method, "GET");
1093        assert_eq!(result.routes[0].path, "/users");
1094
1095        // Check spec info includes servers
1096        assert_eq!(result.spec_info.servers.len(), 2);
1097        assert!(result.spec_info.servers.contains(&"https://api.example.com/v1".to_string()));
1098        assert!(result.spec_info.servers.contains(&"https://dev.example.com/v1".to_string()));
1099    }
1100
1101    #[test]
1102    fn test_import_openapi_with_invalid_json() {
1103        let invalid_openapi_json = r#"{
1104            "openapi": "3.0.3",
1105            "info": {
1106                "title": "Test API",
1107                "version": "1.0.0"
1108            },
1109            "paths": {
1110                "/users": {
1111                    "get": {
1112                        "responses": {
1113                            "200": {
1114                                "description": "Success"
1115                            }
1116                        }
1117                    }
1118                }
1119            }
1120        }"#;
1121
1122        let result = import_openapi_spec(invalid_openapi_json, None);
1123        // Should handle gracefully and return default response
1124        assert!(result.is_ok());
1125        assert_eq!(result.unwrap().routes.len(), 1);
1126    }
1127
1128    #[test]
1129    fn test_import_openapi_with_no_responses() {
1130        let openapi_json = r#"{
1131            "openapi": "3.0.3",
1132            "info": {
1133                "title": "Test API",
1134                "version": "1.0.0"
1135            },
1136            "paths": {
1137                "/users": {
1138                    "get": {
1139                        "operationId": "getUsers",
1140                        "responses": {}
1141                    }
1142                }
1143            }
1144        }"#;
1145
1146        let result = import_openapi_spec(openapi_json, None);
1147        // Should handle missing responses gracefully
1148        assert!(result.is_ok());
1149        let routes = result.unwrap().routes;
1150        assert_eq!(routes.len(), 1);
1151        assert_eq!(routes[0].response.status, 200); // Default status
1152    }
1153}