allframe_core/router/
openapi.rs1use serde_json::{json, Value};
8
9use crate::router::{RouteMetadata, Router};
10
11#[derive(Debug, Clone)]
16pub struct OpenApiServer {
17 pub url: String,
19 pub description: Option<String>,
21}
22
23impl OpenApiServer {
24 pub fn new(url: impl Into<String>) -> Self {
26 Self {
27 url: url.into(),
28 description: None,
29 }
30 }
31
32 pub fn with_description(mut self, description: impl Into<String>) -> Self {
34 self.description = Some(description.into());
35 self
36 }
37}
38
39pub struct OpenApiGenerator {
43 title: String,
44 version: String,
45 description: Option<String>,
46 servers: Vec<OpenApiServer>,
47}
48
49impl OpenApiGenerator {
50 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
52 Self {
53 title: title.into(),
54 version: version.into(),
55 description: None,
56 servers: vec![],
57 }
58 }
59
60 pub fn with_description(mut self, description: impl Into<String>) -> Self {
62 self.description = Some(description.into());
63 self
64 }
65
66 pub fn with_server(
79 mut self,
80 url: impl Into<String>,
81 description: Option<impl Into<String>>,
82 ) -> Self {
83 let mut server = OpenApiServer::new(url);
84 if let Some(desc) = description {
85 server = server.with_description(desc);
86 }
87 self.servers.push(server);
88 self
89 }
90
91 pub fn with_servers(mut self, servers: Vec<OpenApiServer>) -> Self {
93 self.servers = servers;
94 self
95 }
96
97 pub fn generate(&self, router: &Router) -> Value {
99 let mut spec = json!({
100 "openapi": "3.1.0",
101 "info": {
102 "title": self.title,
103 "version": self.version,
104 },
105 "paths": {}
106 });
107
108 if let Some(ref desc) = self.description {
110 spec["info"]["description"] = Value::String(desc.clone());
111 }
112
113 if !self.servers.is_empty() {
115 let servers: Vec<Value> = self
116 .servers
117 .iter()
118 .map(|s| {
119 let mut server = json!({ "url": s.url });
120 if let Some(ref desc) = s.description {
121 server["description"] = Value::String(desc.clone());
122 }
123 server
124 })
125 .collect();
126 spec["servers"] = Value::Array(servers);
127 }
128
129 let paths = self.build_paths(router.routes());
131 spec["paths"] = paths;
132
133 spec
134 }
135
136 fn build_paths(&self, routes: &[RouteMetadata]) -> Value {
137 let mut paths = serde_json::Map::new();
138
139 for route in routes {
140 if route.protocol != "rest" {
142 continue;
143 }
144
145 let path_item = paths.entry(route.path.clone()).or_insert_with(|| json!({}));
146
147 let method = route.method.to_lowercase();
148 let operation = self.build_operation(route);
149
150 if let Value::Object(ref mut map) = path_item {
151 map.insert(method, operation);
152 }
153 }
154
155 Value::Object(paths)
156 }
157
158 fn build_operation(&self, route: &RouteMetadata) -> Value {
159 let mut operation = json!({
160 "responses": {
161 "200": {
162 "description": "Successful response"
163 }
164 }
165 });
166
167 if let Some(ref desc) = route.description {
169 operation["description"] = Value::String(desc.clone());
170 }
171
172 if let Some(ref schema) = route.request_schema {
174 operation["requestBody"] = json!({
175 "required": true,
176 "content": {
177 "application/json": {
178 "schema": schema
179 }
180 }
181 });
182 }
183
184 if let Some(ref schema) = route.response_schema {
186 operation["responses"]["200"]["content"] = json!({
187 "application/json": {
188 "schema": schema
189 }
190 });
191 }
192
193 operation
194 }
195}
196
197impl Router {
198 pub fn to_openapi(&self, title: &str, version: &str) -> Value {
203 OpenApiGenerator::new(title, version).generate(self)
204 }
205
206 pub fn to_openapi_with_description(
208 &self,
209 title: &str,
210 version: &str,
211 description: &str,
212 ) -> Value {
213 OpenApiGenerator::new(title, version)
214 .with_description(description)
215 .generate(self)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::router::RouteMetadata;
223
224 #[tokio::test]
225 async fn test_openapi_generator_basic() {
226 let generator = OpenApiGenerator::new("Test API", "1.0.0");
227 let router = Router::new();
228
229 let spec = generator.generate(&router);
230
231 assert_eq!(spec["openapi"], "3.1.0");
232 assert_eq!(spec["info"]["title"], "Test API");
233 assert_eq!(spec["info"]["version"], "1.0.0");
234 assert!(spec["paths"].is_object());
235 }
236
237 #[tokio::test]
238 async fn test_openapi_with_description() {
239 let generator = OpenApiGenerator::new("Test API", "1.0.0").with_description("A test API");
240 let router = Router::new();
241
242 let spec = generator.generate(&router);
243
244 assert_eq!(spec["info"]["description"], "A test API");
245 }
246
247 #[tokio::test]
248 async fn test_openapi_single_route() {
249 let mut router = Router::new();
250 router.get("/users", || async { "Users".to_string() });
251
252 let spec = router.to_openapi("Test API", "1.0.0");
253
254 assert!(spec["paths"]["/users"].is_object());
255 assert!(spec["paths"]["/users"]["get"].is_object());
256 assert!(spec["paths"]["/users"]["get"]["responses"]["200"].is_object());
257 }
258
259 #[tokio::test]
260 async fn test_openapi_multiple_routes() {
261 let mut router = Router::new();
262 router.get("/users", || async { "List".to_string() });
263 router.post("/users", || async { "Create".to_string() });
264 router.get("/posts", || async { "Posts".to_string() });
265
266 let spec = router.to_openapi("Test API", "1.0.0");
267
268 assert!(spec["paths"]["/users"]["get"].is_object());
269 assert!(spec["paths"]["/users"]["post"].is_object());
270 assert!(spec["paths"]["/posts"]["get"].is_object());
271 }
272
273 #[tokio::test]
274 async fn test_openapi_route_with_description() {
275 let mut router = Router::new();
276 let metadata =
277 RouteMetadata::new("/users", "GET", "rest").with_description("Get all users");
278 router.add_route(metadata);
279
280 let spec = router.to_openapi("Test API", "1.0.0");
281
282 assert_eq!(
283 spec["paths"]["/users"]["get"]["description"],
284 "Get all users"
285 );
286 }
287
288 #[tokio::test]
289 async fn test_openapi_route_with_request_schema() {
290 let mut router = Router::new();
291 let request_schema = serde_json::json!({
292 "type": "object",
293 "properties": {
294 "name": {"type": "string"}
295 }
296 });
297
298 let metadata = RouteMetadata::new("/users", "POST", "rest")
299 .with_request_schema(request_schema.clone());
300 router.add_route(metadata);
301
302 let spec = router.to_openapi("Test API", "1.0.0");
303
304 assert_eq!(
305 spec["paths"]["/users"]["post"]["requestBody"]["content"]["application/json"]["schema"],
306 request_schema
307 );
308 }
309
310 #[tokio::test]
311 async fn test_openapi_route_with_response_schema() {
312 let mut router = Router::new();
313 let response_schema = serde_json::json!({
314 "type": "object",
315 "properties": {
316 "id": {"type": "string"},
317 "name": {"type": "string"}
318 }
319 });
320
321 let metadata = RouteMetadata::new("/users", "GET", "rest")
322 .with_response_schema(response_schema.clone());
323 router.add_route(metadata);
324
325 let spec = router.to_openapi("Test API", "1.0.0");
326
327 assert_eq!(
328 spec["paths"]["/users"]["get"]["responses"]["200"]["content"]["application/json"]
329 ["schema"],
330 response_schema
331 );
332 }
333
334 #[tokio::test]
335 async fn test_openapi_filters_non_rest_routes() {
336 let mut router = Router::new();
337 router.add_route(RouteMetadata::new("/users", "GET", "rest"));
338 router.add_route(RouteMetadata::new("users", "query", "graphql"));
339 router.add_route(RouteMetadata::new("UserService", "unary", "grpc"));
340
341 let spec = router.to_openapi("Test API", "1.0.0");
342
343 assert!(spec["paths"]["/users"].is_object());
345 assert!(spec["paths"]["users"].is_null());
346 assert!(spec["paths"]["UserService"].is_null());
347 }
348
349 #[tokio::test]
350 async fn test_router_to_openapi_convenience_method() {
351 let mut router = Router::new();
352 router.get("/test", || async { "Test".to_string() });
353
354 let spec = router.to_openapi("My API", "2.0.0");
355
356 assert_eq!(spec["info"]["title"], "My API");
357 assert_eq!(spec["info"]["version"], "2.0.0");
358 assert!(spec["paths"]["/test"]["get"].is_object());
359 }
360
361 #[tokio::test]
362 async fn test_router_to_openapi_with_description() {
363 let mut router = Router::new();
364 router.get("/test", || async { "Test".to_string() });
365
366 let spec = router.to_openapi_with_description("My API", "2.0.0", "A great API");
367
368 assert_eq!(spec["info"]["description"], "A great API");
369 }
370}