openapi_from_source/
openapi_builder.rs

1use crate::extractor::{HttpMethod, RouteInfo};
2use crate::schema_generator::{Schema, SchemaGenerator};
3use log::debug;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// OpenAPI document builder
8pub struct OpenApiBuilder {
9    /// OpenAPI info section
10    info: Info,
11    /// Paths collection (URL path -> PathItem)
12    paths: HashMap<String, PathItem>,
13    /// Components section (schemas, etc.)
14    components: Components,
15}
16
17/// OpenAPI Info object
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Info {
20    /// API title
21    pub title: String,
22    /// API version
23    pub version: String,
24    /// API description
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub description: Option<String>,
27}
28
29/// OpenAPI PathItem object - represents all operations for a single path
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct PathItem {
32    /// GET operation
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub get: Option<Operation>,
35    /// POST operation
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub post: Option<Operation>,
38    /// PUT operation
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub put: Option<Operation>,
41    /// DELETE operation
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub delete: Option<Operation>,
44    /// PATCH operation
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub patch: Option<Operation>,
47    /// OPTIONS operation
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub options: Option<Operation>,
50    /// HEAD operation
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub head: Option<Operation>,
53}
54
55/// OpenAPI Operation object - represents a single API operation
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Operation {
58    /// Operation summary
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub summary: Option<String>,
61    /// Operation description
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub description: Option<String>,
64    /// Operation ID
65    #[serde(rename = "operationId", skip_serializing_if = "Option::is_none")]
66    pub operation_id: Option<String>,
67    /// Parameters (path, query, header)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub parameters: Option<Vec<Parameter>>,
70    /// Request body
71    #[serde(rename = "requestBody", skip_serializing_if = "Option::is_none")]
72    pub request_body: Option<RequestBody>,
73    /// Responses
74    pub responses: HashMap<String, Response>,
75}
76
77/// OpenAPI Parameter object
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct Parameter {
80    /// Parameter name
81    pub name: String,
82    /// Parameter location (path, query, header)
83    #[serde(rename = "in")]
84    pub location: String,
85    /// Whether the parameter is required
86    pub required: bool,
87    /// Parameter schema
88    pub schema: Schema,
89    /// Parameter description
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub description: Option<String>,
92}
93
94/// OpenAPI RequestBody object
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct RequestBody {
97    /// Request body description
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub description: Option<String>,
100    /// Whether the request body is required
101    pub required: bool,
102    /// Content types and their schemas
103    pub content: HashMap<String, MediaType>,
104}
105
106/// OpenAPI MediaType object
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct MediaType {
109    /// Schema for this media type
110    pub schema: Schema,
111}
112
113/// OpenAPI Response object
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct Response {
116    /// Response description
117    pub description: String,
118    /// Response content
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub content: Option<HashMap<String, MediaType>>,
121}
122
123/// OpenAPI Components object
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Components {
126    /// Schema definitions
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub schemas: Option<HashMap<String, Schema>>,
129}
130
131/// Complete OpenAPI document
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct OpenApiDocument {
134    /// OpenAPI version
135    pub openapi: String,
136    /// API info
137    pub info: Info,
138    /// API paths
139    pub paths: HashMap<String, PathItem>,
140    /// Components (schemas, etc.)
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub components: Option<Components>,
143}
144
145impl OpenApiBuilder {
146    /// Create a new OpenApiBuilder with default info
147    pub fn new() -> Self {
148        debug!("Initializing OpenApiBuilder");
149        Self {
150            info: Info {
151                title: "Generated API".to_string(),
152                version: "1.0.0".to_string(),
153                description: Some("API documentation generated from Rust code".to_string()),
154            },
155            paths: HashMap::new(),
156            components: Components { schemas: None },
157        }
158    }
159
160    /// Set custom info for the API
161    pub fn with_info(mut self, title: String, version: String, description: Option<String>) -> Self {
162        self.info = Info {
163            title,
164            version,
165            description,
166        };
167        self
168    }
169
170    /// Add a route to the OpenAPI document
171    pub fn add_route(&mut self, route: &RouteInfo, schema_gen: &mut SchemaGenerator) {
172        debug!("Adding route: {} {}", route.method_str(), route.path);
173
174        // Convert path parameters from :param to {param} format
175        let openapi_path = Self::convert_path_format(&route.path);
176
177        // Generate parameters
178        let parameters = if route.parameters.is_empty() {
179            None
180        } else {
181            let params: Vec<Parameter> = route
182                .parameters
183                .iter()
184                .map(|p| {
185                    let param_schema = schema_gen.generate_parameter_schema(p);
186                    Parameter {
187                        name: param_schema.name,
188                        location: param_schema.location,
189                        required: param_schema.required,
190                        schema: param_schema.schema,
191                        description: None,
192                    }
193                })
194                .collect();
195            Some(params)
196        };
197
198        // Generate request body if present
199        let request_body = route.request_body.as_ref().map(|type_info| {
200            let schema = schema_gen.generate_schema(type_info);
201            RequestBody {
202                description: Some("Request body".to_string()),
203                required: true,
204                content: {
205                    let mut content = HashMap::new();
206                    content.insert(
207                        "application/json".to_string(),
208                        MediaType { schema },
209                    );
210                    content
211                },
212            }
213        });
214
215        // Generate response
216        let response = if let Some(response_type) = &route.response_type {
217            let schema = schema_gen.generate_schema(response_type);
218            Response {
219                description: "Successful response".to_string(),
220                content: Some({
221                    let mut content = HashMap::new();
222                    content.insert(
223                        "application/json".to_string(),
224                        MediaType { schema },
225                    );
226                    content
227                }),
228            }
229        } else {
230            // Default response when type is unknown
231            Response {
232                description: "Successful response".to_string(),
233                content: None,
234            }
235        };
236
237        let mut responses = HashMap::new();
238        responses.insert("200".to_string(), response);
239
240        // Create the operation
241        let operation = Operation {
242            summary: Some(format!("{} {}", route.method_str(), route.path)),
243            description: None,
244            operation_id: Some(route.handler_name.clone()),
245            parameters,
246            request_body,
247            responses,
248        };
249
250        // Add operation to the appropriate path and method
251        let path_item = self.paths.entry(openapi_path).or_insert_with(|| PathItem {
252            get: None,
253            post: None,
254            put: None,
255            delete: None,
256            patch: None,
257            options: None,
258            head: None,
259        });
260
261        match route.method {
262            HttpMethod::Get => path_item.get = Some(operation),
263            HttpMethod::Post => path_item.post = Some(operation),
264            HttpMethod::Put => path_item.put = Some(operation),
265            HttpMethod::Delete => path_item.delete = Some(operation),
266            HttpMethod::Patch => path_item.patch = Some(operation),
267            HttpMethod::Options => path_item.options = Some(operation),
268            HttpMethod::Head => path_item.head = Some(operation),
269        }
270    }
271
272    /// Convert path format from :param or {param} to OpenAPI {param} format
273    fn convert_path_format(path: &str) -> String {
274        // Handle both Axum style (:param) and Actix style ({param})
275        // Convert :param to {param}
276        let parts: Vec<&str> = path.split('/').collect();
277        let converted_parts: Vec<String> = parts
278            .iter()
279            .map(|part| {
280                if part.starts_with(':') {
281                    format!("{{{}}}", &part[1..])
282                } else {
283                    part.to_string()
284                }
285            })
286            .collect();
287        
288        converted_parts.join("/")
289    }
290
291    /// Build the final OpenAPI document
292    pub fn build(self, schema_gen: SchemaGenerator) -> OpenApiDocument {
293        debug!("Building final OpenAPI document");
294
295        // Collect all schemas from the schema generator
296        let schemas = schema_gen.get_schemas();
297        let components = if !schemas.is_empty() {
298            Some(Components {
299                schemas: Some(schemas.clone()),
300            })
301        } else {
302            None
303        };
304
305        OpenApiDocument {
306            openapi: "3.0.0".to_string(),
307            info: self.info,
308            paths: self.paths,
309            components,
310        }
311    }
312}
313
314impl Default for OpenApiBuilder {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320impl RouteInfo {
321    /// Get the HTTP method as a string
322    fn method_str(&self) -> &str {
323        match self.method {
324            HttpMethod::Get => "GET",
325            HttpMethod::Post => "POST",
326            HttpMethod::Put => "PUT",
327            HttpMethod::Delete => "DELETE",
328            HttpMethod::Patch => "PATCH",
329            HttpMethod::Options => "OPTIONS",
330            HttpMethod::Head => "HEAD",
331        }
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::extractor::{HttpMethod, Parameter, ParameterLocation, RouteInfo, TypeInfo};
339    use crate::parser::AstParser;
340    use crate::type_resolver::TypeResolver;
341    use std::fs;
342    use std::io::Write;
343    use tempfile::TempDir;
344
345    /// Helper function to create a temporary file with content
346    fn create_temp_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
347        let file_path = dir.path().join(name);
348        let mut file = fs::File::create(&file_path).unwrap();
349        file.write_all(content.as_bytes()).unwrap();
350        file_path
351    }
352
353    /// Helper function to create a SchemaGenerator from code
354    fn create_generator_from_code(code: &str) -> SchemaGenerator {
355        let temp_dir = TempDir::new().unwrap();
356        let file_path = create_temp_file(&temp_dir, "test.rs", code);
357        let parsed = AstParser::parse_file(&file_path).unwrap();
358        let type_resolver = TypeResolver::new(vec![parsed]);
359        SchemaGenerator::new(type_resolver)
360    }
361
362    #[test]
363    fn test_new_builder() {
364        let builder = OpenApiBuilder::new();
365        
366        assert_eq!(builder.info.title, "Generated API");
367        assert_eq!(builder.info.version, "1.0.0");
368        assert!(builder.info.description.is_some());
369        assert!(builder.paths.is_empty());
370    }
371
372    #[test]
373    fn test_with_info() {
374        let builder = OpenApiBuilder::new()
375            .with_info(
376                "My API".to_string(),
377                "2.0.0".to_string(),
378                Some("Custom description".to_string()),
379            );
380        
381        assert_eq!(builder.info.title, "My API");
382        assert_eq!(builder.info.version, "2.0.0");
383        assert_eq!(builder.info.description, Some("Custom description".to_string()));
384    }
385
386    #[test]
387    fn test_add_simple_get_route() {
388        let mut builder = OpenApiBuilder::new();
389        let mut schema_gen = create_generator_from_code("");
390        
391        let route = RouteInfo::new(
392            "/users".to_string(),
393            HttpMethod::Get,
394            "get_users".to_string(),
395        );
396        
397        builder.add_route(&route, &mut schema_gen);
398        
399        assert_eq!(builder.paths.len(), 1);
400        assert!(builder.paths.contains_key("/users"));
401        
402        let path_item = &builder.paths["/users"];
403        assert!(path_item.get.is_some());
404        assert!(path_item.post.is_none());
405        
406        let operation = path_item.get.as_ref().unwrap();
407        assert_eq!(operation.operation_id, Some("get_users".to_string()));
408        assert!(operation.parameters.is_none());
409        assert!(operation.request_body.is_none());
410        assert!(operation.responses.contains_key("200"));
411    }
412
413    #[test]
414    fn test_add_post_route_with_request_body() {
415        let code = r#"
416            pub struct User {
417                pub id: u32,
418                pub name: String,
419            }
420        "#;
421        
422        let mut builder = OpenApiBuilder::new();
423        let mut schema_gen = create_generator_from_code(code);
424        
425        let mut route = RouteInfo::new(
426            "/users".to_string(),
427            HttpMethod::Post,
428            "create_user".to_string(),
429        );
430        route.request_body = Some(TypeInfo::new("User".to_string()));
431        
432        builder.add_route(&route, &mut schema_gen);
433        
434        let path_item = &builder.paths["/users"];
435        assert!(path_item.post.is_some());
436        
437        let operation = path_item.post.as_ref().unwrap();
438        assert!(operation.request_body.is_some());
439        
440        let request_body = operation.request_body.as_ref().unwrap();
441        assert!(request_body.required);
442        assert!(request_body.content.contains_key("application/json"));
443    }
444
445    #[test]
446    fn test_add_route_with_path_parameter() {
447        let mut builder = OpenApiBuilder::new();
448        let mut schema_gen = create_generator_from_code("");
449        
450        let mut route = RouteInfo::new(
451            "/users/:id".to_string(),
452            HttpMethod::Get,
453            "get_user".to_string(),
454        );
455        route.parameters.push(Parameter::new(
456            "id".to_string(),
457            ParameterLocation::Path,
458            TypeInfo::new("u32".to_string()),
459            true,
460        ));
461        
462        builder.add_route(&route, &mut schema_gen);
463        
464        // Path should be converted to OpenAPI format
465        assert!(builder.paths.contains_key("/users/{id}"));
466        
467        let path_item = &builder.paths["/users/{id}"];
468        let operation = path_item.get.as_ref().unwrap();
469        
470        assert!(operation.parameters.is_some());
471        let parameters = operation.parameters.as_ref().unwrap();
472        assert_eq!(parameters.len(), 1);
473        assert_eq!(parameters[0].name, "id");
474        assert_eq!(parameters[0].location, "path");
475        assert!(parameters[0].required);
476    }
477
478    #[test]
479    fn test_add_route_with_query_parameter() {
480        let mut builder = OpenApiBuilder::new();
481        let mut schema_gen = create_generator_from_code("");
482        
483        let mut route = RouteInfo::new(
484            "/users".to_string(),
485            HttpMethod::Get,
486            "list_users".to_string(),
487        );
488        route.parameters.push(Parameter::new(
489            "page".to_string(),
490            ParameterLocation::Query,
491            TypeInfo::new("i32".to_string()),
492            false,
493        ));
494        
495        builder.add_route(&route, &mut schema_gen);
496        
497        let path_item = &builder.paths["/users"];
498        let operation = path_item.get.as_ref().unwrap();
499        
500        assert!(operation.parameters.is_some());
501        let parameters = operation.parameters.as_ref().unwrap();
502        assert_eq!(parameters.len(), 1);
503        assert_eq!(parameters[0].name, "page");
504        assert_eq!(parameters[0].location, "query");
505        assert!(!parameters[0].required);
506    }
507
508    #[test]
509    fn test_add_route_with_response_type() {
510        let code = r#"
511            pub struct User {
512                pub id: u32,
513                pub name: String,
514            }
515        "#;
516        
517        let mut builder = OpenApiBuilder::new();
518        let mut schema_gen = create_generator_from_code(code);
519        
520        let mut route = RouteInfo::new(
521            "/users/:id".to_string(),
522            HttpMethod::Get,
523            "get_user".to_string(),
524        );
525        route.response_type = Some(TypeInfo::new("User".to_string()));
526        
527        builder.add_route(&route, &mut schema_gen);
528        
529        let path_item = &builder.paths["/users/{id}"];
530        let operation = path_item.get.as_ref().unwrap();
531        
532        let response = &operation.responses["200"];
533        assert_eq!(response.description, "Successful response");
534        assert!(response.content.is_some());
535        
536        let content = response.content.as_ref().unwrap();
537        assert!(content.contains_key("application/json"));
538    }
539
540    #[test]
541    fn test_add_multiple_routes_same_path() {
542        let mut builder = OpenApiBuilder::new();
543        let mut schema_gen = create_generator_from_code("");
544        
545        let get_route = RouteInfo::new(
546            "/users".to_string(),
547            HttpMethod::Get,
548            "list_users".to_string(),
549        );
550        
551        let post_route = RouteInfo::new(
552            "/users".to_string(),
553            HttpMethod::Post,
554            "create_user".to_string(),
555        );
556        
557        builder.add_route(&get_route, &mut schema_gen);
558        builder.add_route(&post_route, &mut schema_gen);
559        
560        // Should have only one path entry
561        assert_eq!(builder.paths.len(), 1);
562        
563        let path_item = &builder.paths["/users"];
564        assert!(path_item.get.is_some());
565        assert!(path_item.post.is_some());
566        
567        assert_eq!(
568            path_item.get.as_ref().unwrap().operation_id,
569            Some("list_users".to_string())
570        );
571        assert_eq!(
572            path_item.post.as_ref().unwrap().operation_id,
573            Some("create_user".to_string())
574        );
575    }
576
577    #[test]
578    fn test_add_routes_different_methods() {
579        let mut builder = OpenApiBuilder::new();
580        let mut schema_gen = create_generator_from_code("");
581        
582        let methods = vec![
583            (HttpMethod::Get, "get_handler"),
584            (HttpMethod::Post, "post_handler"),
585            (HttpMethod::Put, "put_handler"),
586            (HttpMethod::Delete, "delete_handler"),
587            (HttpMethod::Patch, "patch_handler"),
588        ];
589        
590        for (method, handler) in methods {
591            let route = RouteInfo::new(
592                "/resource".to_string(),
593                method,
594                handler.to_string(),
595            );
596            builder.add_route(&route, &mut schema_gen);
597        }
598        
599        let path_item = &builder.paths["/resource"];
600        assert!(path_item.get.is_some());
601        assert!(path_item.post.is_some());
602        assert!(path_item.put.is_some());
603        assert!(path_item.delete.is_some());
604        assert!(path_item.patch.is_some());
605    }
606
607    #[test]
608    fn test_convert_path_format_axum_style() {
609        let path = "/users/:id/posts/:post_id";
610        let converted = OpenApiBuilder::convert_path_format(path);
611        assert_eq!(converted, "/users/{id}/posts/{post_id}");
612    }
613
614    #[test]
615    fn test_convert_path_format_actix_style() {
616        let path = "/users/{id}/posts/{post_id}";
617        let converted = OpenApiBuilder::convert_path_format(path);
618        assert_eq!(converted, "/users/{id}/posts/{post_id}");
619    }
620
621    #[test]
622    fn test_convert_path_format_no_params() {
623        let path = "/users/list";
624        let converted = OpenApiBuilder::convert_path_format(path);
625        assert_eq!(converted, "/users/list");
626    }
627
628    #[test]
629    fn test_build_document_structure() {
630        let code = r#"
631            pub struct User {
632                pub id: u32,
633                pub name: String,
634            }
635        "#;
636        
637        let mut builder = OpenApiBuilder::new();
638        let mut schema_gen = create_generator_from_code(code);
639        
640        let mut route = RouteInfo::new(
641            "/users".to_string(),
642            HttpMethod::Post,
643            "create_user".to_string(),
644        );
645        route.request_body = Some(TypeInfo::new("User".to_string()));
646        
647        builder.add_route(&route, &mut schema_gen);
648        
649        let document = builder.build(schema_gen);
650        
651        assert_eq!(document.openapi, "3.0.0");
652        assert_eq!(document.info.title, "Generated API");
653        assert_eq!(document.info.version, "1.0.0");
654        assert_eq!(document.paths.len(), 1);
655        assert!(document.components.is_some());
656        
657        let components = document.components.unwrap();
658        assert!(components.schemas.is_some());
659        
660        let schemas = components.schemas.unwrap();
661        assert!(schemas.contains_key("User"));
662    }
663
664    #[test]
665    fn test_build_document_with_multiple_schemas() {
666        let code = r#"
667            pub struct User {
668                pub id: u32,
669                pub profile: Profile,
670            }
671            
672            pub struct Profile {
673                pub bio: String,
674            }
675        "#;
676        
677        let mut builder = OpenApiBuilder::new();
678        let mut schema_gen = create_generator_from_code(code);
679        
680        let mut route = RouteInfo::new(
681            "/users".to_string(),
682            HttpMethod::Post,
683            "create_user".to_string(),
684        );
685        route.request_body = Some(TypeInfo::new("User".to_string()));
686        
687        builder.add_route(&route, &mut schema_gen);
688        
689        let document = builder.build(schema_gen);
690        
691        let components = document.components.unwrap();
692        let schemas = components.schemas.unwrap();
693        
694        // Both User and Profile should be in schemas
695        assert!(schemas.contains_key("User"));
696        assert!(schemas.contains_key("Profile"));
697    }
698
699    #[test]
700    fn test_build_document_no_schemas() {
701        let mut builder = OpenApiBuilder::new();
702        let mut schema_gen = create_generator_from_code("");
703        
704        let route = RouteInfo::new(
705            "/health".to_string(),
706            HttpMethod::Get,
707            "health_check".to_string(),
708        );
709        
710        builder.add_route(&route, &mut schema_gen);
711        
712        let document = builder.build(schema_gen);
713        
714        // Components should be None when there are no schemas
715        assert!(document.components.is_none());
716    }
717
718    #[test]
719    fn test_operation_summary_format() {
720        let mut builder = OpenApiBuilder::new();
721        let mut schema_gen = create_generator_from_code("");
722        
723        let route = RouteInfo::new(
724            "/users/:id".to_string(),
725            HttpMethod::Get,
726            "get_user".to_string(),
727        );
728        
729        builder.add_route(&route, &mut schema_gen);
730        
731        let path_item = &builder.paths["/users/{id}"];
732        let operation = path_item.get.as_ref().unwrap();
733        
734        assert_eq!(operation.summary, Some("GET /users/:id".to_string()));
735    }
736
737    #[test]
738    fn test_default_response_without_type() {
739        let mut builder = OpenApiBuilder::new();
740        let mut schema_gen = create_generator_from_code("");
741        
742        let route = RouteInfo::new(
743            "/users".to_string(),
744            HttpMethod::Delete,
745            "delete_user".to_string(),
746        );
747        
748        builder.add_route(&route, &mut schema_gen);
749        
750        let path_item = &builder.paths["/users"];
751        let operation = path_item.delete.as_ref().unwrap();
752        
753        let response = &operation.responses["200"];
754        assert_eq!(response.description, "Successful response");
755        assert!(response.content.is_none());
756    }
757
758    #[test]
759    fn test_complex_route_with_all_features() {
760        let code = r#"
761            pub struct CreateUserRequest {
762                pub name: String,
763                pub email: String,
764            }
765            
766            pub struct User {
767                pub id: u32,
768                pub name: String,
769                pub email: String,
770            }
771        "#;
772        
773        let mut builder = OpenApiBuilder::new();
774        let mut schema_gen = create_generator_from_code(code);
775        
776        let mut route = RouteInfo::new(
777            "/users".to_string(),
778            HttpMethod::Post,
779            "create_user".to_string(),
780        );
781        route.request_body = Some(TypeInfo::new("CreateUserRequest".to_string()));
782        route.response_type = Some(TypeInfo::new("User".to_string()));
783        route.parameters.push(Parameter::new(
784            "api_key".to_string(),
785            ParameterLocation::Header,
786            TypeInfo::new("String".to_string()),
787            true,
788        ));
789        
790        builder.add_route(&route, &mut schema_gen);
791        
792        let path_item = &builder.paths["/users"];
793        let operation = path_item.post.as_ref().unwrap();
794        
795        // Check parameters
796        assert!(operation.parameters.is_some());
797        let parameters = operation.parameters.as_ref().unwrap();
798        assert_eq!(parameters.len(), 1);
799        assert_eq!(parameters[0].location, "header");
800        
801        // Check request body
802        assert!(operation.request_body.is_some());
803        
804        // Check response
805        let response = &operation.responses["200"];
806        assert!(response.content.is_some());
807        
808        // Build and check schemas
809        let document = builder.build(schema_gen);
810        let schemas = document.components.unwrap().schemas.unwrap();
811        assert!(schemas.contains_key("CreateUserRequest"));
812        assert!(schemas.contains_key("User"));
813    }
814
815    #[test]
816    fn test_multiple_paths_in_document() {
817        let mut builder = OpenApiBuilder::new();
818        let mut schema_gen = create_generator_from_code("");
819        
820        let routes = vec![
821            ("/users", HttpMethod::Get, "list_users"),
822            ("/users/:id", HttpMethod::Get, "get_user"),
823            ("/posts", HttpMethod::Get, "list_posts"),
824            ("/posts/:id", HttpMethod::Get, "get_post"),
825        ];
826        
827        for (path, method, handler) in routes {
828            let route = RouteInfo::new(
829                path.to_string(),
830                method,
831                handler.to_string(),
832            );
833            builder.add_route(&route, &mut schema_gen);
834        }
835        
836        let document = builder.build(schema_gen);
837        
838        assert_eq!(document.paths.len(), 4);
839        assert!(document.paths.contains_key("/users"));
840        assert!(document.paths.contains_key("/users/{id}"));
841        assert!(document.paths.contains_key("/posts"));
842        assert!(document.paths.contains_key("/posts/{id}"));
843    }
844}