diff --git a/gotham_restful/Cargo.toml b/gotham_restful/Cargo.toml index 512cbb1..78359fe 100644 --- a/gotham_restful/Cargo.toml +++ b/gotham_restful/Cargo.toml @@ -27,7 +27,7 @@ gotham_restful_derive = { version = "0.0.4-dev" } indexmap = { version = "1.3.2", optional = true } itertools = "0.9.0" jsonwebtoken = { version = "7.1.0", optional = true } -log = { version = "0.4.8", optional = true } +log = "0.4.8" mime = "0.3.16" openapiv3 = { version = "0.3", optional = true } serde = { version = "1.0.106", features = ["derive"] } @@ -44,7 +44,7 @@ default = [] auth = ["gotham_restful_derive/auth", "base64", "cookie", "jsonwebtoken"] errorlog = [] database = ["gotham_restful_derive/database", "gotham_middleware_diesel"] -openapi = ["gotham_restful_derive/openapi", "indexmap", "log", "openapiv3"] +openapi = ["gotham_restful_derive/openapi", "indexmap", "openapiv3"] [package.metadata.docs.rs] all-features = true diff --git a/gotham_restful/src/lib.rs b/gotham_restful/src/lib.rs index 6c1a5fd..c62ce0b 100644 --- a/gotham_restful/src/lib.rs +++ b/gotham_restful/src/lib.rs @@ -110,6 +110,7 @@ Licensed under your option of: extern crate self as gotham_restful; #[macro_use] extern crate gotham_derive; +#[macro_use] extern crate log; #[macro_use] extern crate serde; #[doc(no_inline)] diff --git a/gotham_restful/src/openapi/builder.rs b/gotham_restful/src/openapi/builder.rs new file mode 100644 index 0000000..88c0ba4 --- /dev/null +++ b/gotham_restful/src/openapi/builder.rs @@ -0,0 +1,96 @@ +use crate::{OpenapiType, OpenapiSchema}; +use indexmap::IndexMap; +use openapiv3::{ + Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema, + Server +}; +use std::sync::{Arc, RwLock}; + +pub struct OpenapiBuilder +{ + pub openapi : Arc> +} + +impl OpenapiBuilder +{ + pub fn new(title : String, version : String, url : String) -> Self + { + Self { + openapi: Arc::new(RwLock::new(OpenAPI { + openapi: "3.0.2".to_string(), + info: openapiv3::Info { + title, version, + ..Default::default() + }, + servers: vec![Server { + url, + ..Default::default() + }], + ..Default::default() + })) + } + } + + /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to + /// modify the path and add it back after the modification + pub fn remove_path(&mut self, path : &str) -> PathItem + { + let mut openapi = self.openapi.write().unwrap(); + match openapi.paths.swap_remove(path) { + Some(Item(item)) => item, + _ => PathItem::default() + } + } + + pub fn add_path(&mut self, path : Path, item : PathItem) + { + let mut openapi = self.openapi.write().unwrap(); + openapi.paths.insert(path.to_string(), Item(item)); + } + + fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) + { + self.add_schema_dependencies(&mut schema.dependencies); + + let mut openapi = self.openapi.write().unwrap(); + match &mut openapi.components { + Some(comp) => { + comp.schemas.insert(name, Item(schema.into_schema())); + }, + None => { + let mut comp = Components::default(); + comp.schemas.insert(name, Item(schema.into_schema())); + openapi.components = Some(comp); + } + }; + } + + fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) + { + let keys : Vec = dependencies.keys().map(|k| k.to_string()).collect(); + for dep in keys + { + let dep_schema = dependencies.swap_remove(&dep); + if let Some(dep_schema) = dep_schema + { + self.add_schema_impl(dep, dep_schema); + } + } + } + + pub fn add_schema(&mut self) -> ReferenceOr + { + let mut schema = T::schema(); + match schema.name.clone() { + Some(name) => { + let reference = Reference { reference: format!("#/components/schemas/{}", name) }; + self.add_schema_impl(name, schema); + reference + }, + None => { + self.add_schema_dependencies(&mut schema.dependencies); + Item(schema.into_schema()) + } + } + } +} diff --git a/gotham_restful/src/openapi/handler.rs b/gotham_restful/src/openapi/handler.rs new file mode 100644 index 0000000..2054b0d --- /dev/null +++ b/gotham_restful/src/openapi/handler.rs @@ -0,0 +1,110 @@ +use super::SECURITY_NAME; +use futures_util::{future, future::FutureExt}; +use gotham::{ + error::Result, + handler::{Handler, HandlerFuture, NewHandler}, + helpers::http::response::create_response, + state::State +}; +use indexmap::IndexMap; +use mime::{APPLICATION_JSON, TEXT_PLAIN}; +use openapiv3::{APIKeyLocation, OpenAPI, ReferenceOr, SecurityScheme}; +use std::{ + pin::Pin, + sync::{Arc, RwLock} +}; + +#[derive(Clone)] +pub struct OpenapiHandler +{ + openapi : Arc> +} + +impl OpenapiHandler +{ + pub fn new(openapi : Arc>) -> Self + { + Self { openapi } + } +} + +impl NewHandler for OpenapiHandler +{ + type Instance = Self; + + fn new_handler(&self) -> Result + { + Ok(self.clone()) + } +} + +#[cfg(feature = "auth")] +fn get_security(state : &mut State) -> IndexMap> +{ + use crate::AuthSource; + use gotham::state::FromState; + + let source = match AuthSource::try_borrow_from(state) { + Some(source) => source, + None => return Default::default() + }; + + let security_scheme = match source { + AuthSource::Cookie(name) => SecurityScheme::APIKey { + location: APIKeyLocation::Cookie, + name: name.to_string() + }, + AuthSource::Header(name) => SecurityScheme::APIKey { + location: APIKeyLocation::Header, + name: name.to_string() + }, + AuthSource::AuthorizationHeader => SecurityScheme::HTTP { + scheme: "bearer".to_owned(), + bearer_format: Some("JWT".to_owned()) + } + }; + + let mut security_schemes : IndexMap> = Default::default(); + security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); + + security_schemes +} + +#[cfg(not(feature = "auth"))] +fn get_security(state : &mut State) -> (Vec, IndexMap>) +{ + Default::default() +} + +impl Handler for OpenapiHandler +{ + fn handle(self, mut state : State) -> Pin> + { + 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, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + return 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); + + match serde_json::to_string(&openapi) { + Ok(body) => { + let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); + future::ok((state, res)).boxed() + }, + Err(e) => { + error!("Unable to handle OpenAPI request due to error: {}", e); + let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); + future::ok((state, res)).boxed() + } + } + } +} diff --git a/gotham_restful/src/openapi/mod.rs b/gotham_restful/src/openapi/mod.rs index 5c19494..aff6b1e 100644 --- a/gotham_restful/src/openapi/mod.rs +++ b/gotham_restful/src/openapi/mod.rs @@ -1,3 +1,7 @@ +const SECURITY_NAME : &str = "authToken"; + +pub mod builder; +pub mod handler; pub mod router; pub mod types; diff --git a/gotham_restful/src/openapi/router.rs b/gotham_restful/src/openapi/router.rs index 3cf3b19..0298656 100644 --- a/gotham_restful/src/openapi/router.rs +++ b/gotham_restful/src/openapi/router.rs @@ -6,220 +6,19 @@ use crate::{ OpenapiType, RequestBody }; -use futures_util::{future, future::FutureExt}; +use super::{builder::OpenapiBuilder, handler::OpenapiHandler, SECURITY_NAME}; use gotham::{ - handler::{Handler, HandlerFuture, NewHandler}, - helpers::http::response::create_response, pipeline::chain::PipelineHandleChain, - router::builder::*, - state::State + router::builder::* }; use indexmap::IndexMap; -use log::error; -use mime::{Mime, APPLICATION_JSON, STAR_STAR, TEXT_PLAIN}; +use mime::{Mime, STAR_STAR}; use openapiv3::{ - APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem, - ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema, - SchemaKind, SecurityScheme, Server, StatusCode, Type + MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, + ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind, + StatusCode, Type }; -use std::{ - panic::RefUnwindSafe, - pin::Pin, - sync::{Arc, RwLock} -}; - -/** -This type is required to build routes while adding them to the generated OpenAPI Spec at the -same time. There is no need to use this type directly. See [`WithOpenapi`] on how to do this. - -[`WithOpenapi`]: trait.WithOpenapi.html -*/ -pub struct OpenapiBuilder -{ - openapi : Arc> -} - -impl OpenapiBuilder -{ - pub fn new(title : String, version : String, url : String) -> Self - { - Self { - openapi: Arc::new(RwLock::new(OpenAPI { - openapi: "3.0.2".to_string(), - info: openapiv3::Info { - title, version, - ..Default::default() - }, - servers: vec![Server { - url, - ..Default::default() - }], - ..Default::default() - })) - } - } - - /// Remove path from the OpenAPI spec, or return an empty one if not included. This is handy if you need to - /// modify the path and add it back after the modification - fn remove_path(&mut self, path : &str) -> PathItem - { - let mut openapi = self.openapi.write().unwrap(); - match openapi.paths.swap_remove(path) { - Some(Item(item)) => item, - _ => PathItem::default() - } - } - - fn add_path(&mut self, path : Path, item : PathItem) - { - let mut openapi = self.openapi.write().unwrap(); - openapi.paths.insert(path.to_string(), Item(item)); - } - - fn add_schema_impl(&mut self, name : String, mut schema : OpenapiSchema) - { - self.add_schema_dependencies(&mut schema.dependencies); - - let mut openapi = self.openapi.write().unwrap(); - match &mut openapi.components { - Some(comp) => { - comp.schemas.insert(name, Item(schema.into_schema())); - }, - None => { - let mut comp = Components::default(); - comp.schemas.insert(name, Item(schema.into_schema())); - openapi.components = Some(comp); - } - }; - } - - fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap) - { - let keys : Vec = dependencies.keys().map(|k| k.to_string()).collect(); - for dep in keys - { - let dep_schema = dependencies.swap_remove(&dep); - if let Some(dep_schema) = dep_schema - { - self.add_schema_impl(dep, dep_schema); - } - } - } - - fn add_schema(&mut self) -> ReferenceOr - { - let mut schema = T::schema(); - match schema.name.clone() { - Some(name) => { - let reference = Reference { reference: format!("#/components/schemas/{}", name) }; - self.add_schema_impl(name, schema); - reference - }, - None => { - self.add_schema_dependencies(&mut schema.dependencies); - Item(schema.into_schema()) - } - } - } -} - -#[derive(Clone)] -struct OpenapiHandler -{ - openapi : Arc> -} - -impl OpenapiHandler -{ - fn new(openapi : Arc>) -> Self - { - Self { openapi } - } -} - -impl NewHandler for OpenapiHandler -{ - type Instance = Self; - - fn new_handler(&self) -> gotham::error::Result - { - Ok(self.clone()) - } -} - -#[cfg(feature = "auth")] -const SECURITY_NAME : &str = "authToken"; - -#[cfg(feature = "auth")] -fn get_security(state : &mut State) -> IndexMap> -{ - use crate::AuthSource; - use gotham::state::FromState; - - let source = match AuthSource::try_borrow_from(state) { - Some(source) => source, - None => return Default::default() - }; - - let security_scheme = match source { - AuthSource::Cookie(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Cookie, - name: name.to_string() - }, - AuthSource::Header(name) => SecurityScheme::APIKey { - location: APIKeyLocation::Header, - name: name.to_string() - }, - AuthSource::AuthorizationHeader => SecurityScheme::HTTP { - scheme: "bearer".to_owned(), - bearer_format: Some("JWT".to_owned()) - } - }; - - let mut security_schemes : IndexMap> = Default::default(); - security_schemes.insert(SECURITY_NAME.to_owned(), ReferenceOr::Item(security_scheme)); - - security_schemes -} - -#[cfg(not(feature = "auth"))] -fn get_security(state : &mut State) -> (Vec, IndexMap>) -{ - Default::default() -} - -impl Handler for OpenapiHandler -{ - fn handle(self, mut state : State) -> Pin> - { - 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, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - return 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); - - match serde_json::to_string(&openapi) { - Ok(body) => { - let res = create_response(&state, crate::StatusCode::OK, APPLICATION_JSON, body); - future::ok((state, res)).boxed() - }, - Err(e) => { - error!("Unable to handle OpenAPI request due to error: {}", e); - let res = create_response(&state, crate::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, ""); - future::ok((state, res)).boxed() - } - } - } -} +use std::panic::RefUnwindSafe; /// This trait adds the `get_openapi` method to an OpenAPI-aware router. pub trait GetOpenapi diff --git a/gotham_restful/src/routing.rs b/gotham_restful/src/routing.rs index 70eafc7..b87089b 100644 --- a/gotham_restful/src/routing.rs +++ b/gotham_restful/src/routing.rs @@ -6,7 +6,7 @@ use crate::{ StatusCode }; #[cfg(feature = "openapi")] -use crate::openapi::router::OpenapiBuilder; +use crate::openapi::builder::OpenapiBuilder; use futures_util::{future, future::FutureExt}; use gotham::{