1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-04-11 10:37:51 +00:00

add swagger_ui to the router

This commit is contained in:
Dominic 2021-02-25 00:37:55 +01:00
parent 7ed98c82e8
commit 28ae4dfdee
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
5 changed files with 230 additions and 53 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New `endpoint` router extension with associated `Endpoint` trait ([!18])
- Support for custom endpoints using the `#[endpoint]` macro ([!19])
- Support for `anyhow::Error` (or any type implementing `Into<HandlerError>`) in most responses
- `swagger_ui` method to the OpenAPI router to render the specification using Swagger UI
### Changed
- The cors handler can now copy headers from the request if desired

View file

@ -20,27 +20,33 @@ include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
[dependencies]
base64 = { version = "0.13.0", optional = true }
chrono = { version = "0.4.19", features = ["serde"], optional = true }
cookie = { version = "0.14", optional = true }
futures-core = "0.3.7"
futures-util = "0.3.7"
gotham = { version = "0.5.0", default-features = false }
gotham_derive = "0.5.0"
gotham_middleware_diesel = { version = "0.2.0", optional = true }
gotham_restful_derive = "0.2.0-dev"
indexmap = { version = "1.3.2", optional = true }
jsonwebtoken = { version = "7.1.0", optional = true }
log = "0.4.8"
mime = "0.3.16"
once_cell = { version = "1.5", optional = true }
openapiv3 = { version = "0.3.2", optional = true }
regex = { version = "1.4", optional = true }
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.58"
thiserror = "1.0"
# features
chrono = { version = "0.4.19", features = ["serde"], optional = true }
uuid = { version = "0.8.1", optional = true }
# non-feature optional dependencies
base64 = { version = "0.13.0", optional = true }
cookie = { version = "0.14", optional = true }
gotham_middleware_diesel = { version = "0.2.0", optional = true }
indexmap = { version = "1.3.2", optional = true }
indoc = { version = "1.0", optional = true }
jsonwebtoken = { version = "7.1.0", optional = true }
once_cell = { version = "1.5", optional = true }
openapiv3 = { version = "0.3.2", optional = true }
regex = { version = "1.4", optional = true }
sha2 = { version = "0.9.3", optional = true }
[dev-dependencies]
diesel = { version = "1.4.4", features = ["postgres"] }
futures-executor = "0.3.5"
@ -61,7 +67,7 @@ errorlog = []
# These features are exclusive - https://gitlab.com/msrd0/gotham-restful/-/issues/4
without-openapi = []
openapi = ["gotham_restful_derive/openapi", "indexmap", "once_cell", "openapiv3", "regex"]
openapi = ["gotham_restful_derive/openapi", "base64", "indexmap", "indoc", "once_cell", "openapiv3", "regex", "sha2"]
[package.metadata.docs.rs]
no-default-features = true

View file

@ -122,6 +122,7 @@ fn main() {
route.resource::<Users>("users");
route.resource::<Auth>("auth");
route.get_openapi("openapi");
route.swagger_ui("");
});
})
);

View file

@ -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)
});

View file

@ -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>>