mirror of
https://gitlab.com/msrd0/gotham-restful.git
synced 2025-05-09 08:00:41 +00:00
add swagger_ui to the router
This commit is contained in:
parent
7ed98c82e8
commit
28ae4dfdee
5 changed files with 230 additions and 53 deletions
|
@ -4,37 +4,26 @@ use futures_util::{future, future::FutureExt};
|
|||
use gotham::{
|
||||
anyhow,
|
||||
handler::{Handler, HandlerFuture, NewHandler},
|
||||
helpers::http::response::create_response,
|
||||
hyper::StatusCode,
|
||||
helpers::http::response::{create_empty_response, create_response},
|
||||
hyper::{
|
||||
header::{
|
||||
HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, ETAG, IF_NONE_MATCH, REFERRER_POLICY,
|
||||
X_CONTENT_TYPE_OPTIONS
|
||||
},
|
||||
Body, Response, StatusCode, Uri
|
||||
},
|
||||
state::State
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use mime::{APPLICATION_JSON, TEXT_PLAIN};
|
||||
use mime::{APPLICATION_JSON, TEXT_HTML, TEXT_PLAIN};
|
||||
use once_cell::sync::Lazy;
|
||||
use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock}
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenapiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for OpenapiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
fn get_security(state: &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>> {
|
||||
use crate::AuthSource;
|
||||
|
@ -71,33 +60,202 @@ fn get_security(_state: &mut State) -> IndexMap<String, ReferenceOr<SecuritySche
|
|||
Default::default()
|
||||
}
|
||||
|
||||
fn create_openapi_response(state: &mut State, openapi: &Arc<RwLock<OpenAPI>>) -> Response<Body> {
|
||||
let openapi = match openapi.read() {
|
||||
Ok(openapi) => openapi,
|
||||
Err(e) => {
|
||||
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||
return create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
}
|
||||
};
|
||||
|
||||
let mut openapi = openapi.clone();
|
||||
let security_schemes = get_security(state);
|
||||
let mut components = openapi.components.unwrap_or_default();
|
||||
components.security_schemes = security_schemes;
|
||||
openapi.components = Some(components);
|
||||
|
||||
match serde_json::to_string(&openapi) {
|
||||
Ok(body) => {
|
||||
let mut res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
|
||||
let headers = res.headers_mut();
|
||||
headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||
res
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
||||
create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenapiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
impl OpenapiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for OpenapiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for OpenapiHandler {
|
||||
fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let openapi = match self.openapi.read() {
|
||||
Ok(openapi) => openapi,
|
||||
Err(e) => {
|
||||
error!("Unable to acquire read lock for the OpenAPI specification: {}", e);
|
||||
let res = create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
return future::ok((state, res)).boxed();
|
||||
}
|
||||
};
|
||||
let res = create_openapi_response(&mut state, &self.openapi);
|
||||
future::ok((state, res)).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
let mut openapi = openapi.clone();
|
||||
let security_schemes = get_security(&mut state);
|
||||
let mut components = openapi.components.unwrap_or_default();
|
||||
components.security_schemes = security_schemes;
|
||||
openapi.components = Some(components);
|
||||
#[derive(Clone)]
|
||||
pub struct SwaggerUiHandler {
|
||||
openapi: Arc<RwLock<OpenAPI>>
|
||||
}
|
||||
|
||||
match serde_json::to_string(&openapi) {
|
||||
Ok(body) => {
|
||||
let res = create_response(&state, StatusCode::OK, APPLICATION_JSON, body);
|
||||
impl SwaggerUiHandler {
|
||||
pub fn new(openapi: Arc<RwLock<OpenAPI>>) -> Self {
|
||||
Self { openapi }
|
||||
}
|
||||
}
|
||||
|
||||
impl NewHandler for SwaggerUiHandler {
|
||||
type Instance = Self;
|
||||
|
||||
fn new_handler(&self) -> anyhow::Result<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler for SwaggerUiHandler {
|
||||
fn handle(self, mut state: State) -> Pin<Box<HandlerFuture>> {
|
||||
let uri: &Uri = state.borrow();
|
||||
let query = uri.query();
|
||||
match query {
|
||||
// TODO this is hacky
|
||||
Some(q) if q.contains("spec") => {
|
||||
let res = create_openapi_response(&mut state, &self.openapi);
|
||||
future::ok((state, res)).boxed()
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Unable to handle OpenAPI request due to error: {}", e);
|
||||
let res = create_response(&state, StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
|
||||
_ => {
|
||||
{
|
||||
let headers: &HeaderMap = state.borrow();
|
||||
if headers
|
||||
.get(IF_NONE_MATCH)
|
||||
.map_or(false, |etag| etag.as_bytes() == SWAGGER_UI_HTML_ETAG.as_bytes())
|
||||
{
|
||||
let res = create_empty_response(&state, StatusCode::NOT_MODIFIED);
|
||||
return future::ok((state, res)).boxed();
|
||||
}
|
||||
}
|
||||
|
||||
let mut res = create_response(&state, StatusCode::OK, TEXT_HTML, SWAGGER_UI_HTML.as_bytes());
|
||||
let headers = res.headers_mut();
|
||||
headers.insert(CACHE_CONTROL, HeaderValue::from_static("public,max-age=2592000"));
|
||||
headers.insert(CONTENT_SECURITY_POLICY, format!("default-src 'none'; script-src 'unsafe-inline' 'sha256-{}' 'strict-dynamic'; style-src 'unsafe-inline' https://cdnjs.cloudflare.com; connect-src 'self'; img-src data:;", SWAGGER_UI_SCRIPT_HASH.as_str()).parse().unwrap());
|
||||
headers.insert(ETAG, SWAGGER_UI_HTML_ETAG.parse().unwrap());
|
||||
headers.insert(REFERRER_POLICY, HeaderValue::from_static("strict-origin-when-cross-origin"));
|
||||
headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
|
||||
future::ok((state, res)).boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inspired by https://github.com/swagger-api/swagger-ui/blob/master/dist/index.html
|
||||
const SWAGGER_UI_HTML: Lazy<&'static String> = Lazy::new(|| {
|
||||
let template = indoc::indoc! {
|
||||
r#"
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui.css" integrity="sha512-sphGjcjFvN5sAW6S28Ge+F9SCzRuc9IVkLinDHu7B1wOUHHFAY5sSQ2Axff+qs7/0GTm0Ifg4i0lQKgM8vdV2w==" crossorigin="anonymous"/>
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*, *::before, *::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script>{{script}}</script>
|
||||
</body>
|
||||
</html>
|
||||
"#
|
||||
};
|
||||
Box::leak(Box::new(template.replace("{{script}}", SWAGGER_UI_SCRIPT)))
|
||||
});
|
||||
static SWAGGER_UI_HTML_ETAG: Lazy<String> = Lazy::new(|| {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(SWAGGER_UI_HTML.as_bytes());
|
||||
let hash = hash.finalize();
|
||||
let hash = base64::encode(hash);
|
||||
format!("\"{}\"", hash)
|
||||
});
|
||||
const SWAGGER_UI_SCRIPT: &str = r#"
|
||||
let s0rdy = false;
|
||||
let s1rdy = false;
|
||||
|
||||
window.onload = function() {
|
||||
const cb = function() {
|
||||
if (!s0rdy || !s1rdy)
|
||||
return;
|
||||
const ui = SwaggerUIBundle({
|
||||
url: window.location.origin + window.location.pathname + '?spec',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: 'StandaloneLayout'
|
||||
});
|
||||
window.ui = ui;
|
||||
};
|
||||
|
||||
const s0 = document.createElement('script');
|
||||
s0.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-bundle.js');
|
||||
s0.setAttribute('integrity', 'sha512-EfK//grBlevo9MrtEDyNvf4SkBA0avHZoVLEuSR2Yl6ymnjcIwClgZ7FXdr/42yGqnhEHxb+Sv/bJeUp26YPRw==');
|
||||
s0.setAttribute('crossorigin', 'anonymous');
|
||||
s0.onload = function() {
|
||||
s0rdy = true;
|
||||
cb();
|
||||
};
|
||||
document.head.appendChild(s0);
|
||||
|
||||
const s1 = document.createElement('script');
|
||||
s1.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.43.0/swagger-ui-standalone-preset.js');
|
||||
s1.setAttribute('integrity', 'sha512-Hbx9NyAhG+P6YNBU9mp6hc6ntRjGYHqo/qae4OHhzPA69xbNmF8n2aJxpzUwdbXbYICO6eor4IhgSfiSQm9OYg==');
|
||||
s1.setAttribute('crossorigin', 'anonymous');
|
||||
s1.onload = function() {
|
||||
s1rdy = true;
|
||||
cb();
|
||||
};
|
||||
document.head.appendChild(s1);
|
||||
};
|
||||
"#;
|
||||
static SWAGGER_UI_SCRIPT_HASH: Lazy<String> = Lazy::new(|| {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(SWAGGER_UI_SCRIPT);
|
||||
let hash = hash.finalize();
|
||||
base64::encode(hash)
|
||||
});
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
|
||||
use super::{
|
||||
builder::OpenapiBuilder,
|
||||
handler::{OpenapiHandler, SwaggerUiHandler},
|
||||
operation::OperationDescription
|
||||
};
|
||||
use crate::{routing::*, EndpointWithSchema, OpenapiType, ResourceResultSchema, ResourceWithSchema};
|
||||
use gotham::{hyper::Method, pipeline::chain::PipelineHandleChain, router::builder::*};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use std::panic::RefUnwindSafe;
|
||||
|
||||
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
|
||||
/// This trait adds the `get_openapi` and `swagger_ui` method to an OpenAPI-aware router.
|
||||
pub trait GetOpenapi {
|
||||
fn get_openapi(&mut self, path: &str);
|
||||
fn swagger_ui(&mut self, path: &str);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -51,6 +56,12 @@ macro_rules! implOpenapiRouter {
|
|||
.get(path)
|
||||
.to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
|
||||
}
|
||||
|
||||
fn swagger_ui(&mut self, path: &str) {
|
||||
self.router
|
||||
.get(path)
|
||||
.to_new_handler(SwaggerUiHandler::new(self.openapi_builder.openapi.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, C, P> DrawResourcesWithSchema for OpenapiRouter<'a, $implType<'b, C, P>>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue