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