1use crate::routers::introspection::{RouteInfo, RouteInspector};
27use hyper::Method;
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31const OPENAPI_VERSION: &str = "3.0.3";
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct OpenApiSpec {
37 pub openapi: String,
39
40 pub info: InfoObject,
42
43 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub servers: Vec<ServerObject>,
46
47 pub paths: HashMap<String, PathItemObject>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub components: Option<ComponentsObject>,
53
54 #[serde(skip_serializing_if = "Vec::is_empty")]
56 pub security: Vec<HashMap<String, Vec<String>>>,
57
58 #[serde(skip_serializing_if = "Vec::is_empty")]
60 pub tags: Vec<TagObject>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct InfoObject {
66 pub title: String,
68
69 pub version: String,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub description: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 #[serde(rename = "termsOfService")]
79 pub terms_of_service: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub contact: Option<ContactObject>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub license: Option<LicenseObject>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ContactObject {
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub name: Option<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub url: Option<String>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub email: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct LicenseObject {
109 pub name: String,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub url: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ServerObject {
120 pub url: String,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub description: Option<String>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PathItemObject {
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub get: Option<OperationObject>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub post: Option<OperationObject>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub put: Option<OperationObject>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub patch: Option<OperationObject>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub delete: Option<OperationObject>,
150
151 #[serde(skip_serializing_if = "Vec::is_empty")]
153 pub parameters: Vec<ParameterObject>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct OperationObject {
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub summary: Option<String>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub description: Option<String>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 #[serde(rename = "operationId")]
170 pub operation_id: Option<String>,
171
172 #[serde(skip_serializing_if = "Vec::is_empty")]
174 pub tags: Vec<String>,
175
176 #[serde(skip_serializing_if = "Vec::is_empty")]
178 pub parameters: Vec<ParameterObject>,
179
180 pub responses: HashMap<String, ResponseObject>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct ParameterObject {
187 pub name: String,
189
190 #[serde(rename = "in")]
192 pub location: String,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub description: Option<String>,
197
198 pub required: bool,
200
201 pub schema: SchemaObject,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ResponseObject {
208 pub description: String,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub content: Option<HashMap<String, MediaTypeObject>>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct MediaTypeObject {
219 pub schema: SchemaObject,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SchemaObject {
226 #[serde(rename = "type")]
228 pub schema_type: String,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
232 pub format: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ComponentsObject {
238 #[serde(skip_serializing_if = "HashMap::is_empty")]
240 pub schemas: HashMap<String, SchemaObject>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct TagObject {
246 pub name: String,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub description: Option<String>,
252}
253
254#[derive(Debug, Clone, Default)]
256pub struct PathItem {
257 operations: HashMap<Method, OperationObject>,
258 parameters: Vec<ParameterObject>,
259}
260
261impl PathItem {
262 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn with_method(
287 mut self,
288 method: Method,
289 summary: impl Into<String>,
290 operation_id: Option<impl Into<String>>,
291 ) -> Self {
292 let operation = OperationObject {
293 summary: Some(summary.into()),
294 description: None,
295 operation_id: operation_id.map(|s| s.into()),
296 tags: Vec::new(),
297 parameters: Vec::new(),
298 responses: HashMap::new(),
299 };
300
301 self.operations.insert(method, operation);
302 self
303 }
304
305 pub fn with_parameter(
316 mut self,
317 name: impl Into<String>,
318 description: impl Into<String>,
319 schema_type: impl Into<String>,
320 ) -> Self {
321 let param = ParameterObject {
322 name: name.into(),
323 location: "path".to_string(),
324 description: Some(description.into()),
325 required: true,
326 schema: SchemaObject {
327 schema_type: schema_type.into(),
328 format: None,
329 },
330 };
331
332 self.parameters.push(param);
333 self
334 }
335
336 fn build(self) -> PathItemObject {
338 let mut path_item = PathItemObject {
339 get: None,
340 post: None,
341 put: None,
342 patch: None,
343 delete: None,
344 parameters: self.parameters,
345 };
346
347 for (method, operation) in self.operations {
348 match method {
349 Method::GET => path_item.get = Some(operation),
350 Method::POST => path_item.post = Some(operation),
351 Method::PUT => path_item.put = Some(operation),
352 Method::PATCH => path_item.patch = Some(operation),
353 Method::DELETE => path_item.delete = Some(operation),
354 _ => {}
355 }
356 }
357
358 path_item
359 }
360}
361
362pub struct OpenApiBuilder {
383 info: InfoObject,
384 servers: Vec<ServerObject>,
385 paths: HashMap<String, PathItemObject>,
386 tags: Vec<TagObject>,
387}
388
389impl OpenApiBuilder {
390 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
400 Self {
401 info: InfoObject {
402 title: title.into(),
403 version: version.into(),
404 description: None,
405 terms_of_service: None,
406 contact: None,
407 license: None,
408 },
409 servers: Vec::new(),
410 paths: HashMap::new(),
411 tags: Vec::new(),
412 }
413 }
414
415 pub fn description(&mut self, description: impl Into<String>) -> &mut Self {
417 self.info.description = Some(description.into());
418 self
419 }
420
421 pub fn add_server(&mut self, url: impl Into<String>, description: Option<String>) -> &mut Self {
423 self.servers.push(ServerObject {
424 url: url.into(),
425 description,
426 });
427 self
428 }
429
430 pub fn add_tag(&mut self, name: impl Into<String>, description: Option<String>) -> &mut Self {
432 self.tags.push(TagObject {
433 name: name.into(),
434 description,
435 });
436 self
437 }
438
439 pub fn add_path(&mut self, path: impl Into<String>, path_item: PathItem) -> &mut Self {
441 self.paths.insert(path.into(), path_item.build());
442 self
443 }
444
445 pub fn build(self) -> OpenApiSpec {
447 OpenApiSpec {
448 openapi: OPENAPI_VERSION.to_string(),
449 info: self.info,
450 servers: self.servers,
451 paths: self.paths,
452 components: None,
453 security: Vec::new(),
454 tags: self.tags,
455 }
456 }
457
458 pub fn from_inspector(
479 title: impl Into<String>,
480 version: impl Into<String>,
481 inspector: &RouteInspector,
482 ) -> Self {
483 let mut builder = Self::new(title, version);
484
485 for namespace in inspector.all_namespaces() {
487 builder.add_tag(&namespace, None);
488 }
489
490 for route in inspector.all_routes() {
492 builder.add_route_info(route);
493 }
494
495 builder
496 }
497
498 fn add_route_info(&mut self, route: &RouteInfo) {
500 let mut path_item = PathItem::new();
501
502 for param in &route.params {
504 path_item = path_item.with_parameter(param, format!("{} parameter", param), "string");
505 }
506
507 for method_str in &route.methods {
509 if let Ok(method) = method_str.parse::<Method>() {
511 let operation_id = route.name.clone();
512 let summary = route
513 .metadata
514 .get("summary")
515 .cloned()
516 .unwrap_or_else(|| format!("{} {}", method.as_str(), route.path));
517
518 path_item = path_item.with_method(method, summary, operation_id);
519 }
520 }
521
522 self.paths.insert(route.path.clone(), path_item.build());
523 }
524}
525
526impl OpenApiSpec {
527 pub fn to_json(&self) -> Result<String, serde_json::Error> {
539 serde_json::to_string_pretty(self)
540 }
541
542 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
554 serde_yaml::to_string(self)
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_openapi_builder_basic() {
564 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
565 builder.description("A test API");
566
567 let spec = builder.build();
568 assert_eq!(spec.info.title, "Test API");
569 assert_eq!(spec.info.version, "1.0.0");
570 assert_eq!(spec.info.description, Some("A test API".to_string()));
571 }
572
573 #[test]
574 fn test_openapi_builder_with_server() {
575 let mut builder = OpenApiBuilder::new("Test API", "1.0.0");
576 builder.add_server(
577 "https://api.example.com",
578 Some("Production server".to_string()),
579 );
580
581 let spec = builder.build();
582 assert_eq!(spec.servers.len(), 1);
583 assert_eq!(spec.servers[0].url, "https://api.example.com");
584 }
585
586 #[test]
587 fn test_path_item_builder() {
588 let path_item = PathItem::new()
589 .with_method(Method::GET, "Get user", Some("users:detail"))
590 .with_parameter("id", "User ID", "string");
591
592 let path_item_obj = path_item.build();
593 assert!(path_item_obj.get.is_some());
594 assert_eq!(path_item_obj.parameters.len(), 1);
595 }
596
597 #[test]
598 fn test_openapi_builder_from_inspector() {
599 let mut inspector = RouteInspector::new();
600 inspector.add_route("/users/", vec![Method::GET], Some("users:list"), None);
601 inspector.add_route(
602 "/users/{id}/",
603 vec![Method::GET],
604 Some("users:detail"),
605 None,
606 );
607
608 let spec = OpenApiBuilder::from_inspector("Test API", "1.0.0", &inspector).build();
609
610 assert_eq!(spec.paths.len(), 2);
611 assert!(spec.paths.contains_key("/users/"));
612 assert!(spec.paths.contains_key("/users/{id}/"));
613 }
614
615 #[test]
616 fn test_openapi_spec_to_json() {
617 let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
618 let json = spec.to_json().unwrap();
619 assert!(json.contains("Test API"));
620 assert!(json.contains("3.0.3"));
621 }
622}