1use crate::{
2 config::OpenApiConfig,
3 error::{OpenApiError, OpenApiResult},
4 schema::{SchemaConfig, SchemaGenerator},
5 specification::*,
6};
7use std::collections::HashMap;
8
9pub struct OpenApiGenerator {
11 config: OpenApiConfig,
13 schema_generator: SchemaGenerator,
15 spec: Option<OpenApiSpec>,
17}
18
19#[derive(Debug, Clone)]
21pub struct RouteMetadata {
22 pub method: String,
24 pub path: String,
26 pub summary: Option<String>,
28 pub description: Option<String>,
30 pub operation_id: Option<String>,
32 pub tags: Vec<String>,
34 pub request_schema: Option<String>,
36 pub response_schemas: HashMap<String, String>,
38 pub parameters: Vec<ParameterInfo>,
40 pub security: Vec<String>,
42 pub deprecated: bool,
44}
45
46#[derive(Debug, Clone)]
48pub struct ParameterInfo {
49 pub name: String,
51 pub location: String,
53 pub param_type: String,
55 pub description: Option<String>,
57 pub required: bool,
59 pub example: Option<serde_json::Value>,
61}
62
63impl OpenApiGenerator {
64 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 pub fn generate(&mut self, routes: &[RouteMetadata]) -> OpenApiResult<&OpenApiSpec> {
79 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 for route in routes {
100 self.process_route(&mut spec, route)?;
101 }
102
103 self.generate_components(&mut spec)?;
105
106 self.spec = Some(spec);
107 Ok(self.spec.as_ref().unwrap())
108 }
109
110 fn process_route(
112 &mut self,
113 spec: &mut OpenApiSpec,
114 route: &RouteMetadata,
115 ) -> OpenApiResult<()> {
116 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 let operation = self.create_operation(route)?;
136
137 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 fn create_operation(&mut self, route: &RouteMetadata) -> OpenApiResult<Operation> {
160 let parameters = route
162 .parameters
163 .iter()
164 .map(|param| self.create_parameter(param))
165 .collect::<OpenApiResult<Vec<_>>>()?;
166
167 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 let responses = self.create_responses(&route.response_schemas)?;
176
177 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 fn create_parameter(&mut self, param: &ParameterInfo) -> OpenApiResult<Parameter> {
209 let schema = self.schema_generator.generate_schema(¶m.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 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 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 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 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 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 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 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 fn convert_security_requirements(&self) -> Vec<SecurityRequirement> {
441 Vec::new()
443 }
444
445 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 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 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 pub fn specification(&self) -> Option<&OpenApiSpec> {
485 self.spec.as_ref()
486 }
487
488 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 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}