elif_openapi/
generator.rs

1use crate::{
2    config::OpenApiConfig,
3    error::{OpenApiError, OpenApiResult},
4    schema::{SchemaConfig, SchemaGenerator},
5    specification::*,
6};
7use std::collections::HashMap;
8
9/// Main OpenAPI specification generator
10pub struct OpenApiGenerator {
11    /// Configuration
12    config: OpenApiConfig,
13    /// Schema generator
14    schema_generator: SchemaGenerator,
15    /// Generated specification
16    spec: Option<OpenApiSpec>,
17}
18
19/// Route information for OpenAPI generation
20#[derive(Debug, Clone)]
21pub struct RouteMetadata {
22    /// HTTP method
23    pub method: String,
24    /// Path pattern
25    pub path: String,
26    /// Operation summary
27    pub summary: Option<String>,
28    /// Operation description  
29    pub description: Option<String>,
30    /// Operation ID
31    pub operation_id: Option<String>,
32    /// Tags for grouping
33    pub tags: Vec<String>,
34    /// Request body schema
35    pub request_schema: Option<String>,
36    /// Response schemas by status code
37    pub response_schemas: HashMap<String, String>,
38    /// Parameters
39    pub parameters: Vec<ParameterInfo>,
40    /// Security requirements
41    pub security: Vec<String>,
42    /// Deprecated flag
43    pub deprecated: bool,
44}
45
46/// Parameter information
47#[derive(Debug, Clone)]
48pub struct ParameterInfo {
49    /// Parameter name
50    pub name: String,
51    /// Parameter location (path, query, header, cookie)
52    pub location: String,
53    /// Parameter type
54    pub param_type: String,
55    /// Description
56    pub description: Option<String>,
57    /// Required flag
58    pub required: bool,
59    /// Example value
60    pub example: Option<serde_json::Value>,
61}
62
63impl OpenApiGenerator {
64    /// Create a new OpenAPI generator
65    pub fn new(config: OpenApiConfig) -> Self {
66        let schema_config = SchemaConfig::new()
67            .with_nullable_optional(config.nullable_optional)
68            .with_examples(config.include_examples);
69
70        Self {
71            schema_generator: SchemaGenerator::new(schema_config),
72            spec: None,
73            config,
74        }
75    }
76
77    /// Generate OpenAPI specification from route metadata
78    pub fn generate(&mut self, routes: &[RouteMetadata]) -> OpenApiResult<&OpenApiSpec> {
79        // Initialize specification
80        let mut spec = OpenApiSpec {
81            openapi: self.config.openapi_version.clone(),
82            info: self.convert_api_info(),
83            servers: self.convert_servers(),
84            paths: HashMap::new(),
85            components: None,
86            security: self.convert_security_requirements(),
87            tags: self.convert_tags(),
88            external_docs: self
89                .config
90                .external_docs
91                .as_ref()
92                .map(|ed| ExternalDocumentation {
93                    url: ed.url.clone(),
94                    description: ed.description.clone(),
95                }),
96        };
97
98        // Generate paths from routes
99        for route in routes {
100            self.process_route(&mut spec, route)?;
101        }
102
103        // Generate components with schemas
104        self.generate_components(&mut spec)?;
105
106        self.spec = Some(spec);
107        Ok(self.spec.as_ref().unwrap())
108    }
109
110    /// Process a single route and add to specification
111    fn process_route(
112        &mut self,
113        spec: &mut OpenApiSpec,
114        route: &RouteMetadata,
115    ) -> OpenApiResult<()> {
116        // Get or create path item
117        let path_item = spec
118            .paths
119            .entry(route.path.clone())
120            .or_insert_with(|| PathItem {
121                summary: None,
122                description: None,
123                get: None,
124                put: None,
125                post: None,
126                delete: None,
127                options: None,
128                head: None,
129                patch: None,
130                trace: None,
131                parameters: Vec::new(),
132            });
133
134        // Create operation
135        let operation = self.create_operation(route)?;
136
137        // Add operation to appropriate method
138        match route.method.to_uppercase().as_str() {
139            "GET" => path_item.get = Some(operation),
140            "POST" => path_item.post = Some(operation),
141            "PUT" => path_item.put = Some(operation),
142            "DELETE" => path_item.delete = Some(operation),
143            "PATCH" => path_item.patch = Some(operation),
144            "OPTIONS" => path_item.options = Some(operation),
145            "HEAD" => path_item.head = Some(operation),
146            "TRACE" => path_item.trace = Some(operation),
147            _ => {
148                return Err(OpenApiError::route_discovery_error(format!(
149                    "Unsupported HTTP method: {}",
150                    route.method
151                )));
152            }
153        }
154
155        Ok(())
156    }
157
158    /// Create operation from route metadata
159    fn create_operation(&mut self, route: &RouteMetadata) -> OpenApiResult<Operation> {
160        // Generate parameters
161        let parameters = route
162            .parameters
163            .iter()
164            .map(|param| self.create_parameter(param))
165            .collect::<OpenApiResult<Vec<_>>>()?;
166
167        // Generate request body
168        let request_body = if let Some(request_schema) = &route.request_schema {
169            Some(self.create_request_body(request_schema)?)
170        } else {
171            None
172        };
173
174        // Generate responses
175        let responses = self.create_responses(&route.response_schemas)?;
176
177        // Generate security requirements
178        let security = if route.security.is_empty() {
179            Vec::new()
180        } else {
181            route
182                .security
183                .iter()
184                .map(|scheme| {
185                    let mut req = HashMap::new();
186                    req.insert(scheme.clone(), Vec::new());
187                    req
188                })
189                .collect()
190        };
191
192        Ok(Operation {
193            tags: route.tags.clone(),
194            summary: route.summary.clone(),
195            description: route.description.clone(),
196            external_docs: None,
197            operation_id: route.operation_id.clone(),
198            parameters,
199            request_body,
200            responses,
201            security,
202            servers: Vec::new(),
203            deprecated: if route.deprecated { Some(true) } else { None },
204        })
205    }
206
207    /// Create parameter from parameter info
208    fn create_parameter(&mut self, param: &ParameterInfo) -> OpenApiResult<Parameter> {
209        // Generate schema for parameter type
210        let schema = self.schema_generator.generate_schema(&param.param_type)?;
211
212        Ok(Parameter {
213            name: param.name.clone(),
214            location: param.location.clone(),
215            description: param.description.clone(),
216            required: Some(param.required),
217            deprecated: None,
218            schema: Some(schema),
219            example: param.example.clone(),
220        })
221    }
222
223    /// Create request body from schema name
224    fn create_request_body(&mut self, schema_name: &str) -> OpenApiResult<RequestBody> {
225        let schema = Schema {
226            reference: Some(format!("#/components/schemas/{}", schema_name)),
227            ..Default::default()
228        };
229
230        let mut content = HashMap::new();
231        content.insert(
232            "application/json".to_string(),
233            MediaType {
234                schema: Some(schema),
235                example: None,
236                examples: HashMap::new(),
237            },
238        );
239
240        Ok(RequestBody {
241            description: Some(format!("Request payload for {}", schema_name)),
242            content,
243            required: Some(true),
244        })
245    }
246
247    /// Create responses from schema mappings
248    fn create_responses(
249        &mut self,
250        response_schemas: &HashMap<String, String>,
251    ) -> OpenApiResult<HashMap<String, Response>> {
252        let mut responses = HashMap::new();
253
254        // Default success response if none specified
255        if response_schemas.is_empty() {
256            responses.insert(
257                "200".to_string(),
258                Response {
259                    description: "Successful operation".to_string(),
260                    headers: HashMap::new(),
261                    content: HashMap::new(),
262                    links: HashMap::new(),
263                },
264            );
265        } else {
266            for (status_code, schema_name) in response_schemas {
267                let schema = Schema {
268                    reference: Some(format!("#/components/schemas/{}", schema_name)),
269                    ..Default::default()
270                };
271
272                let mut content = HashMap::new();
273                content.insert(
274                    "application/json".to_string(),
275                    MediaType {
276                        schema: Some(schema),
277                        example: None,
278                        examples: HashMap::new(),
279                    },
280                );
281
282                let description = match status_code.as_str() {
283                    "200" => "OK",
284                    "201" => "Created",
285                    "204" => "No Content",
286                    "400" => "Bad Request",
287                    "401" => "Unauthorized",
288                    "403" => "Forbidden",
289                    "404" => "Not Found",
290                    "422" => "Unprocessable Entity",
291                    "500" => "Internal Server Error",
292                    _ => "Response",
293                };
294
295                responses.insert(
296                    status_code.clone(),
297                    Response {
298                        description: description.to_string(),
299                        headers: HashMap::new(),
300                        content,
301                        links: HashMap::new(),
302                    },
303                );
304            }
305        }
306
307        Ok(responses)
308    }
309
310    /// Generate components section with schemas
311    fn generate_components(&mut self, spec: &mut OpenApiSpec) -> OpenApiResult<()> {
312        let schemas = self.schema_generator.get_schemas().clone();
313        let security_schemes = self.convert_security_schemes();
314
315        if !schemas.is_empty() || !security_schemes.is_empty() {
316            spec.components = Some(Components {
317                schemas,
318                responses: HashMap::new(),
319                parameters: HashMap::new(),
320                examples: HashMap::new(),
321                request_bodies: HashMap::new(),
322                headers: HashMap::new(),
323                security_schemes,
324                links: HashMap::new(),
325            });
326        }
327
328        Ok(())
329    }
330
331    /// Convert configuration info to specification info
332    fn convert_api_info(&self) -> ApiInfo {
333        ApiInfo {
334            title: self.config.info.title.clone(),
335            description: self.config.info.description.clone(),
336            terms_of_service: self.config.info.terms_of_service.clone(),
337            contact: self.config.info.contact.as_ref().map(|c| Contact {
338                name: c.name.clone(),
339                url: c.url.clone(),
340                email: c.email.clone(),
341            }),
342            license: self.config.info.license.as_ref().map(|l| License {
343                name: l.name.clone(),
344                url: l.url.clone(),
345            }),
346            version: self.config.info.version.clone(),
347        }
348    }
349
350    /// Convert server configurations
351    fn convert_servers(&self) -> Vec<Server> {
352        self.config
353            .servers
354            .iter()
355            .map(|s| Server {
356                url: s.url.clone(),
357                description: s.description.clone(),
358                variables: s.variables.as_ref().map(|vars| {
359                    vars.iter()
360                        .map(|(k, v)| {
361                            (
362                                k.clone(),
363                                ServerVariable {
364                                    default: v.default.clone(),
365                                    enum_values: v.r#enum.clone(),
366                                    description: v.description.clone(),
367                                },
368                            )
369                        })
370                        .collect()
371                }),
372            })
373            .collect()
374    }
375
376    /// Convert security schemes
377    fn convert_security_schemes(&self) -> HashMap<String, SecurityScheme> {
378        self.config
379            .security_schemes
380            .iter()
381            .map(|(name, scheme)| {
382                let security_scheme = match scheme {
383                    crate::config::SecurityScheme::Http {
384                        scheme,
385                        bearer_format,
386                    } => SecurityScheme::Http {
387                        scheme: scheme.clone(),
388                        bearer_format: bearer_format.clone(),
389                    },
390                    crate::config::SecurityScheme::ApiKey { name, r#in } => {
391                        SecurityScheme::ApiKey {
392                            name: name.clone(),
393                            location: r#in.clone(),
394                        }
395                    }
396                    crate::config::SecurityScheme::OAuth2 { flows } => SecurityScheme::OAuth2 {
397                        flows: OAuth2Flows {
398                            implicit: flows.implicit.as_ref().map(|f| OAuth2Flow {
399                                authorization_url: f.authorization_url.clone(),
400                                token_url: f.token_url.clone(),
401                                refresh_url: f.refresh_url.clone(),
402                                scopes: f.scopes.clone(),
403                            }),
404                            password: flows.password.as_ref().map(|f| OAuth2Flow {
405                                authorization_url: f.authorization_url.clone(),
406                                token_url: f.token_url.clone(),
407                                refresh_url: f.refresh_url.clone(),
408                                scopes: f.scopes.clone(),
409                            }),
410                            client_credentials: flows.client_credentials.as_ref().map(|f| {
411                                OAuth2Flow {
412                                    authorization_url: f.authorization_url.clone(),
413                                    token_url: f.token_url.clone(),
414                                    refresh_url: f.refresh_url.clone(),
415                                    scopes: f.scopes.clone(),
416                                }
417                            }),
418                            authorization_code: flows.authorization_code.as_ref().map(|f| {
419                                OAuth2Flow {
420                                    authorization_url: f.authorization_url.clone(),
421                                    token_url: f.token_url.clone(),
422                                    refresh_url: f.refresh_url.clone(),
423                                    scopes: f.scopes.clone(),
424                                }
425                            }),
426                        },
427                    },
428                    crate::config::SecurityScheme::OpenIdConnect {
429                        open_id_connect_url,
430                    } => SecurityScheme::OpenIdConnect {
431                        open_id_connect_url: open_id_connect_url.clone(),
432                    },
433                };
434                (name.clone(), security_scheme)
435            })
436            .collect()
437    }
438
439    /// Convert global security requirements
440    fn convert_security_requirements(&self) -> Vec<SecurityRequirement> {
441        // Default to no global security (will be set per operation)
442        Vec::new()
443    }
444
445    /// Convert tags
446    fn convert_tags(&self) -> Vec<Tag> {
447        self.config
448            .tags
449            .iter()
450            .map(|t| Tag {
451                name: t.name.clone(),
452                description: t.description.clone(),
453                external_docs: t.external_docs.as_ref().map(|ed| ExternalDocumentation {
454                    url: ed.url.clone(),
455                    description: ed.description.clone(),
456                }),
457            })
458            .collect()
459    }
460
461    /// Export specification as JSON
462    pub fn export_json(&self, pretty: bool) -> OpenApiResult<String> {
463        let spec = self.spec.as_ref().ok_or_else(|| {
464            OpenApiError::generic("No specification generated yet. Call generate() first.")
465        })?;
466
467        if pretty {
468            serde_json::to_string_pretty(spec).map_err(OpenApiError::from)
469        } else {
470            serde_json::to_string(spec).map_err(OpenApiError::from)
471        }
472    }
473
474    /// Export specification as YAML
475    pub fn export_yaml(&self) -> OpenApiResult<String> {
476        let spec = self.spec.as_ref().ok_or_else(|| {
477            OpenApiError::generic("No specification generated yet. Call generate() first.")
478        })?;
479
480        serde_yaml::to_string(spec).map_err(OpenApiError::from)
481    }
482
483    /// Get the generated specification
484    pub fn specification(&self) -> Option<&OpenApiSpec> {
485        self.spec.as_ref()
486    }
487
488    /// Validate the generated specification
489    pub fn validate(&self) -> OpenApiResult<()> {
490        let _spec = self.spec.as_ref().ok_or_else(|| {
491            OpenApiError::validation_error("No specification generated yet. Call generate() first.")
492        })?;
493
494        // Basic validation
495        // TODO: Add more comprehensive validation
496        Ok(())
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::config::OpenApiConfig;
504
505    #[test]
506    fn test_generator_creation() {
507        let config = OpenApiConfig::default();
508        let generator = OpenApiGenerator::new(config);
509        assert!(generator.spec.is_none());
510    }
511
512    #[test]
513    fn test_empty_routes_generation() {
514        let config = OpenApiConfig::new("Test API", "1.0.0");
515        let mut generator = OpenApiGenerator::new(config);
516
517        let routes = vec![];
518        let spec = generator.generate(&routes).unwrap();
519
520        assert_eq!(spec.info.title, "Test API");
521        assert_eq!(spec.info.version, "1.0.0");
522        assert!(spec.paths.is_empty());
523    }
524
525    #[test]
526    fn test_basic_route_generation() {
527        let config = OpenApiConfig::new("Test API", "1.0.0");
528        let mut generator = OpenApiGenerator::new(config);
529
530        let routes = vec![RouteMetadata {
531            method: "GET".to_string(),
532            path: "/users".to_string(),
533            summary: Some("List users".to_string()),
534            description: Some("Get all users".to_string()),
535            operation_id: Some("listUsers".to_string()),
536            tags: vec!["Users".to_string()],
537            request_schema: None,
538            response_schemas: HashMap::new(),
539            parameters: Vec::new(),
540            security: Vec::new(),
541            deprecated: false,
542        }];
543
544        let spec = generator.generate(&routes).unwrap();
545
546        assert_eq!(spec.paths.len(), 1);
547        assert!(spec.paths.contains_key("/users"));
548
549        let path_item = &spec.paths["/users"];
550        assert!(path_item.get.is_some());
551
552        let operation = path_item.get.as_ref().unwrap();
553        assert_eq!(operation.summary, Some("List users".to_string()));
554        assert_eq!(operation.tags, vec!["Users".to_string()]);
555    }
556}