rust_webx_openapi/
openapi.rs1use rust_webx_core::route::scan::RouteEntry;
8use serde_json::{json, Value as JsonValue};
9use std::collections::BTreeSet;
10
11pub fn generate_openapi_spec(title: &str, version: &str) -> JsonValue {
15 let mut paths = serde_json::Map::new();
16 let mut tags_set = BTreeSet::new();
17
18 for entry in inventory::iter::<RouteEntry> {
19 let entry = entry.clone();
20 let method = entry.method.as_str().to_lowercase();
21 let tag = extract_tag(entry.path);
22 tags_set.insert(tag.clone());
23
24 let mut parameters = Vec::new();
25 let mut has_body = false;
26
27 for param in entry.params {
28 if param.source == "body" {
29 has_body = true;
30 } else {
31 parameters.push(json!({
32 "name": param.name,
33 "in": param.source,
34 "required": true,
35 "schema": { "type": param.type_hint },
36 }));
37 }
38 }
39
40 let operation_id = to_operation_id(entry.handler_type);
41
42 let mut operation = json!({
43 "operationId": operation_id,
44 "summary": entry.summary,
45 "responses": build_responses(entry.rsp_type),
46 "tags": [tag],
47 });
48
49 if !entry.description.is_empty() {
50 operation["description"] = json!(entry.description);
51 }
52
53 if !parameters.is_empty() {
54 operation["parameters"] = json!(parameters);
55 }
56
57 if has_body {
58 operation["requestBody"] = json!({
59 "required": true,
60 "content": {
61 "application/json": {
62 "schema": {
63 "type": "object",
64 }
65 }
66 }
67 });
68 }
69
70 let path_entry = paths
71 .entry(entry.path.to_string())
72 .or_insert_with(|| json!({}));
73 if let Some(obj) = path_entry.as_object_mut() {
74 obj.insert(method, operation);
75 }
76 }
77
78 let tags: Vec<JsonValue> = tags_set.iter().map(|t| json!({"name": t})).collect();
79
80 json!({
81 "openapi": "3.0.3",
82 "info": {
83 "title": title,
84 "version": version,
85 },
86 "tags": tags,
87 "paths": paths,
88 "components": {
89 "securitySchemes": {
90 "bearerAuth": {
91 "type": "http",
92 "scheme": "bearer",
93 "bearerFormat": "JWT",
94 }
95 }
96 }
97 })
98}
99
100fn build_responses(rsp_type: &str) -> JsonValue {
102 let schema = if rsp_type == "()" || rsp_type == "unknown" {
103 json!({ "type": "object" })
104 } else if rsp_type == "String" {
105 json!({ "type": "string" })
106 } else if rsp_type.starts_with("Vec<") {
107 let inner = &rsp_type[4..rsp_type.len() - 1];
108 json!({
109 "type": "array",
110 "items": { "$ref": format!("#/components/schemas/{}", inner) }
111 })
112 } else {
113 json!({ "$ref": format!("#/components/schemas/{}", rsp_type) })
114 };
115
116 json!({
117 "200": {
118 "description": "Successful response",
119 "content": {
120 "application/json": { "schema": schema }
121 }
122 },
123 "400": { "description": "Bad request — validation or deserialization error" },
124 "404": { "description": "Resource not found" },
125 "500": { "description": "Internal server error" },
126 })
127}
128
129fn extract_tag(path: &str) -> String {
136 let tag = path
137 .trim_start_matches('/')
138 .split('/')
139 .rfind(|s| !s.starts_with('{') && !s.is_empty())
140 .unwrap_or("default");
141 titlecase(tag)
142}
143
144fn to_operation_id(handler_type: &str) -> String {
147 let name = handler_type.strip_suffix("Request").unwrap_or(handler_type);
148 let mut chars = name.chars();
149 match chars.next() {
150 None => String::new(),
151 Some(c) => c.to_ascii_lowercase().to_string() + chars.as_str(),
152 }
153}
154
155fn titlecase(s: &str) -> String {
156 let mut chars = s.chars();
157 match chars.next() {
158 None => String::new(),
159 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
160 }
161}