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:
parent
7ed98c82e8
commit
28ae4dfdee
5 changed files with 230 additions and 53 deletions
|
@ -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
|
||||
|
|
26
Cargo.toml
26
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -122,6 +122,7 @@ fn main() {
|
|||
route.resource::<Users>("users");
|
||||
route.resource::<Auth>("auth");
|
||||
route.get_openapi("openapi");
|
||||
route.swagger_ui("");
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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
Reference in a new issue