use async_trait::async_trait;
use reinhardt_http::Handler;
use reinhardt_http::{Request, Response, Result};
use reinhardt_rest::openapi::endpoints::generate_openapi_schema;
use reinhardt_rest::openapi::{RedocUI, SwaggerUI};
use reinhardt_urls::prelude::Route;
use reinhardt_urls::routers::Router;
use std::sync::Arc;
pub struct OpenApiRouter<H> {
inner: H,
openapi_json: Arc<String>,
swagger_html: Arc<String>,
redoc_html: Arc<String>,
}
impl<H> OpenApiRouter<H> {
pub fn wrap(handler: H) -> Self {
let schema = generate_openapi_schema();
let openapi_json =
serde_json::to_string_pretty(&schema).expect("Failed to serialize OpenAPI schema");
let swagger_ui = SwaggerUI::new(schema.clone());
let swagger_html = swagger_ui
.render_html()
.expect("Failed to render Swagger UI");
let redoc_ui = RedocUI::new(schema);
let redoc_html = redoc_ui.render_html().expect("Failed to render Redoc UI");
Self {
inner: handler,
openapi_json: Arc::new(openapi_json),
swagger_html: Arc::new(swagger_html),
redoc_html: Arc::new(redoc_html),
}
}
pub fn inner(&self) -> &H {
&self.inner
}
}
#[async_trait]
impl<H: Handler> Handler for OpenApiRouter<H> {
async fn handle(&self, request: Request) -> Result<Response> {
match request.uri.path() {
"/api/openapi.json" => {
let json = (*self.openapi_json).clone();
Ok(Response::ok()
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(json))
}
"/api/docs" => {
let html = (*self.swagger_html).clone();
Ok(Response::ok()
.with_header("Content-Type", "text/html; charset=utf-8")
.with_body(html))
}
"/api/redoc" => {
let html = (*self.redoc_html).clone();
Ok(Response::ok()
.with_header("Content-Type", "text/html; charset=utf-8")
.with_body(html))
}
_ => {
self.inner.handle(request).await
}
}
}
}
impl<H> Router for OpenApiRouter<H>
where
H: Handler + Router,
{
fn add_route(&mut self, _route: Route) {
panic!(
"Cannot add routes to OpenApiRouter after wrapping. \
Add routes to the base router before calling OpenApiRouter::wrap()."
);
}
fn mount(&mut self, _prefix: &str, _routes: Vec<Route>, _namespace: Option<String>) {
panic!(
"Cannot mount routes in OpenApiRouter after wrapping. \
Mount routes in the base router before calling OpenApiRouter::wrap()."
);
}
async fn route(&self, request: Request) -> Result<Response> {
match request.uri.path() {
"/api/openapi.json" => {
let json = (*self.openapi_json).clone();
Ok(Response::ok()
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(json))
}
"/api/docs" => {
let html = (*self.swagger_html).clone();
Ok(Response::ok()
.with_header("Content-Type", "text/html; charset=utf-8")
.with_body(html))
}
"/api/redoc" => {
let html = (*self.redoc_html).clone();
Ok(Response::ok()
.with_header("Content-Type", "text/html; charset=utf-8")
.with_body(html))
}
_ => {
self.inner.route(request).await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hyper::StatusCode;
use rstest::rstest;
struct DummyHandler;
#[async_trait]
impl Handler for DummyHandler {
async fn handle(&self, _request: Request) -> Result<Response> {
Ok(Response::new(StatusCode::OK).with_body("Hello from inner handler"))
}
}
#[rstest]
#[tokio::test]
async fn test_openapi_json_endpoint() {
let handler = DummyHandler;
let wrapped = OpenApiRouter::wrap(handler);
let request = Request::builder().uri("/api/openapi.json").build().unwrap();
let response = wrapped.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
let body_str = String::from_utf8(response.body.to_vec()).unwrap();
assert!(body_str.contains("openapi"));
assert!(body_str.contains("3.")); }
#[rstest]
#[tokio::test]
async fn test_swagger_docs_endpoint() {
let handler = DummyHandler;
let wrapped = OpenApiRouter::wrap(handler);
let request = Request::builder().uri("/api/docs").build().unwrap();
let response = wrapped.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
let body_str = String::from_utf8(response.body.to_vec()).unwrap();
assert!(body_str.contains("swagger-ui"));
}
#[rstest]
#[tokio::test]
async fn test_redoc_docs_endpoint() {
let handler = DummyHandler;
let wrapped = OpenApiRouter::wrap(handler);
let request = Request::builder().uri("/api/redoc").build().unwrap();
let response = wrapped.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
let body_str = String::from_utf8(response.body.to_vec()).unwrap();
assert!(body_str.contains("redoc"));
}
#[rstest]
#[tokio::test]
async fn test_delegation_to_inner_handler() {
let handler = DummyHandler;
let wrapped = OpenApiRouter::wrap(handler);
let request = Request::builder().uri("/some/other/path").build().unwrap();
let response = wrapped.handle(request).await.unwrap();
assert_eq!(response.status, StatusCode::OK);
let body_str = String::from_utf8(response.body.to_vec()).unwrap();
assert_eq!(body_str, "Hello from inner handler");
}
}