rustango/openapi/
router.rs1use std::sync::Arc;
13
14use axum::extract::State;
15use axum::http::header;
16use axum::response::{Html, IntoResponse, Response};
17use axum::routing::get;
18use axum::Router;
19
20use super::OpenApiSpec;
21
22#[must_use]
24pub fn openapi_router(spec: OpenApiSpec) -> Router {
25 Router::new()
26 .route("/openapi.json", get(serve_spec))
27 .route("/docs", get(swagger_ui))
28 .route("/redoc", get(redoc))
29 .with_state(Arc::new(spec))
30}
31
32async fn serve_spec(State(spec): State<Arc<OpenApiSpec>>) -> Response {
33 let body = spec.to_json();
34 (
35 [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
36 body,
37 )
38 .into_response()
39}
40
41async fn swagger_ui() -> Html<&'static str> {
42 Html(SWAGGER_HTML)
43}
44
45async fn redoc() -> Html<&'static str> {
46 Html(REDOC_HTML)
47}
48
49const SWAGGER_HTML: &str = r##"<!doctype html>
50<html lang="en">
51<head>
52<meta charset="utf-8">
53<meta name="viewport" content="width=device-width, initial-scale=1">
54<title>API Docs</title>
55<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
56</head>
57<body>
58<div id="swagger-ui"></div>
59<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
60<script>
61 window.onload = () => {
62 window.ui = SwaggerUIBundle({
63 url: "/openapi.json",
64 dom_id: "#swagger-ui",
65 deepLinking: true,
66 });
67 };
68</script>
69</body>
70</html>
71"##;
72
73const REDOC_HTML: &str = r#"<!doctype html>
74<html lang="en">
75<head>
76<meta charset="utf-8">
77<meta name="viewport" content="width=device-width, initial-scale=1">
78<title>API Docs</title>
79<style>body { margin: 0; }</style>
80</head>
81<body>
82<redoc spec-url="/openapi.json"></redoc>
83<script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"></script>
84</body>
85</html>
86"#;
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::openapi::{Operation, PathItem, Response as OpenApiResponse, Schema};
92 use axum::body::Body;
93 use axum::http::Request;
94 use tower::ServiceExt;
95
96 fn sample_spec() -> OpenApiSpec {
97 OpenApiSpec::new("Demo", "1.2.3")
98 .add_path(
99 "/ping",
100 PathItem::new().get(
101 Operation::new()
102 .summary("Ping")
103 .response("200", OpenApiResponse::new("pong").json_content(Schema::string())),
104 ),
105 )
106 }
107
108 #[tokio::test]
109 async fn openapi_json_returns_serialized_spec() {
110 let app = openapi_router(sample_spec());
111 let resp = app
112 .oneshot(
113 Request::builder()
114 .uri("/openapi.json")
115 .body(Body::empty())
116 .unwrap(),
117 )
118 .await
119 .unwrap();
120 assert_eq!(resp.status(), 200);
121 assert_eq!(
122 resp.headers()
123 .get(header::CONTENT_TYPE)
124 .unwrap()
125 .to_str()
126 .unwrap(),
127 "application/json; charset=utf-8"
128 );
129 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20).await.unwrap();
130 let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
131 assert_eq!(v["info"]["title"], "Demo");
132 assert_eq!(v["info"]["version"], "1.2.3");
133 assert_eq!(v["paths"]["/ping"]["get"]["summary"], "Ping");
134 }
135
136 #[tokio::test]
137 async fn docs_serves_swagger_ui_html() {
138 let app = openapi_router(sample_spec());
139 let resp = app
140 .oneshot(Request::builder().uri("/docs").body(Body::empty()).unwrap())
141 .await
142 .unwrap();
143 assert_eq!(resp.status(), 200);
144 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20).await.unwrap();
145 let body = std::str::from_utf8(&bytes).unwrap();
146 assert!(body.contains("swagger-ui"));
147 assert!(body.contains("/openapi.json"));
148 }
149
150 #[tokio::test]
151 async fn redoc_serves_redoc_html() {
152 let app = openapi_router(sample_spec());
153 let resp = app
154 .oneshot(Request::builder().uri("/redoc").body(Body::empty()).unwrap())
155 .await
156 .unwrap();
157 assert_eq!(resp.status(), 200);
158 let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20).await.unwrap();
159 let body = std::str::from_utf8(&bytes).unwrap();
160 assert!(body.contains("redoc"));
161 assert!(body.contains("/openapi.json"));
162 }
163}