1use axum::{routing::get, Json, Router};
2use serde::Serialize;
3use serde_json::{json, Value};
4
5#[derive(Debug, Clone, Serialize)]
6pub struct OpenApiRoute {
7 pub method: String,
8 pub path: String,
9}
10
11#[derive(Debug, Clone, Serialize)]
12pub struct OpenApiDoc {
13 pub title: String,
14 pub version: String,
15 pub routes: Vec<OpenApiRoute>,
16}
17
18impl OpenApiDoc {
19 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
20 Self {
21 title: title.into(),
22 version: version.into(),
23 routes: Vec::new(),
24 }
25 }
26
27 pub fn add_route(mut self, method: impl Into<String>, path: impl Into<String>) -> Self {
28 self.routes.push(OpenApiRoute {
29 method: method.into(),
30 path: path.into(),
31 });
32 self
33 }
34
35 pub fn to_openapi_json(&self) -> Value {
36 let mut paths = serde_json::Map::new();
37 for route in &self.routes {
38 let method = route.method.to_lowercase();
39 let entry = paths.entry(route.path.clone()).or_insert_with(|| json!({}));
40 let obj = entry.as_object_mut().expect("path entry object");
41 obj.insert(
42 method,
43 json!({
44 "responses": {
45 "200": { "description": "OK" }
46 }
47 }),
48 );
49 }
50
51 json!({
52 "openapi": "3.1.0",
53 "info": {
54 "title": self.title,
55 "version": self.version
56 },
57 "paths": paths
58 })
59 }
60}
61
62pub fn docs_router(doc: OpenApiDoc) -> Router {
63 let openapi = doc.to_openapi_json();
64 let docs_html = r#"<!doctype html>
65<html>
66<head><meta charset="utf-8"><title>NestForge API Docs</title></head>
67<body>
68 <h1>NestForge API Docs</h1>
69 <p>OpenAPI JSON is available at <code>/openapi.json</code>.</p>
70</body>
71</html>"#;
72
73 Router::new()
74 .route(
75 "/openapi.json",
76 get({
77 let payload = openapi.clone();
78 move || async move { Json(payload.clone()) }
79 }),
80 )
81 .route("/docs", get(move || async move { docs_html }))
82}