1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-05-13 10:00:41 +00:00

start moving openapi into its own crate

This commit is contained in:
Dominic 2020-03-30 22:30:04 +02:00
parent 91f7b09fbf
commit 1f0be034be
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
15 changed files with 93 additions and 74 deletions

View file

@ -0,0 +1,33 @@
# -*- eval: (cargo-minor-mode 1) -*-
[package]
name = "gotham_restful_openapi"
version = "0.0.1"
authors = ["Dominic Meiser <git@msrd0.de>"]
edition = "2018"
description = "RESTful additions for Gotham - OpenAPI Support"
keywords = ["gotham", "rest", "restful", "openapi", "swagger"]
license = "EPL-2.0 OR Apache-2.0"
readme = "README.md"
repository = "https://gitlab.com/msrd0/gotham-restful"
[badges]
gitlab = { repository = "msrd0/gotham-restful", branch = "master" }
codecov = { repository = "msrd0/gotham-restful", branch = "master", service = "gitlab" }
[dependencies]
futures = "0.1.29"
gotham = "0.4"
gotham_derive = "0.4"
gotham_restful = "0.0.3"
gotham_restful_derive = { version = "0.0.2", features = ["openapi"] }
hyper = "0.12.35"
indexmap = "1.3.0"
log = "0.4.8"
mime = "0.3.16"
openapiv3 = "0.3"
serde = { version = "1.0.104", features = ["derive"] }
serde_json = "1.0.45"
[dev-dependencies]
thiserror = "1"

View file

@ -0,0 +1 @@
../LICENSE-Apache

View file

@ -0,0 +1 @@
../LICENSE-EPL

View file

@ -0,0 +1 @@
../LICENSE.md

View file

@ -0,0 +1 @@
../README.md

View file

@ -0,0 +1,14 @@
mod router;
pub use router::GetOpenapi;
mod types;
pub use types::{OpenapiType, OpenapiSchema};
/// Not public API
#[doc(hidden)]
pub mod export
{
pub use indexmap::IndexMap;
pub use openapiv3 as openapi;
}

View file

@ -0,0 +1,720 @@
use crate::types::*;
use futures::future::ok;
use gotham::{
extractor::QueryStringExtractor,
handler::{Handler, HandlerFuture, NewHandler},
helpers::http::response::create_response,
pipeline::{
chain::PipelineHandleChain,
set::PipelineSet
},
router::{
builder::*,
tree::node::Node
},
state::State
};
use gotham_restful::*;
use hyper::Body;
use indexmap::IndexMap;
use log::error;
use mime::{Mime, APPLICATION_JSON, TEXT_PLAIN};
use openapiv3::{
APIKeyLocation, Components, MediaType, OpenAPI, Operation, Parameter, ParameterData, ParameterSchemaOrContent, PathItem,
Paths, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, RequestBody as OARequestBody, Response, Responses, Schema,
SchemaKind, SecurityRequirement, SecurityScheme, Server, StatusCode, Type
};
use serde::de::DeserializeOwned;
use std::panic::RefUnwindSafe;
/// This trait adds the `with_openapi` method to gotham's routing. It turns the default
/// router into one that will only allow RESTful resources, but record them and generate
/// an OpenAPI specification on request.
pub trait WithOpenapi<C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce(OpenapiRouter<C, P>),
Title : ToString,
Version : ToString,
Url : ToString;
}
impl<C, P, T> WithOpenapi<C, P> for T
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static,
T : DrawRoutes<C, P>
{
fn with_openapi<F, Title, Version, Url>(&mut self, title : Title, version : Version, server_url : Url, block : F)
where
F : FnOnce(OpenapiRouter<C, P>),
Title : ToString,
Version : ToString,
Url : ToString
{
let router = OpenapiRouter::new(self, OpenapiBuilder::new(title, version, server_url));
block(router);
}
}
/// A helper struct for `OpenapiRouter` to build the OpenAPI specification.
pub struct OpenapiBuilder(OpenAPI);
impl OpenapiBuilder
{
pub fn new<Title : ToString, Version : ToString, Url : ToString>(title : Title, version : Version, server_url : Url) -> Self
{
Self(OpenAPI {
openapi: "3.0.2".to_string(),
info: openapiv3::Info {
title: title.to_string(),
description: None,
terms_of_service: None,
contact: None,
license: None,
version: version.to_string()
},
servers: vec![Server {
url: server_url.to_string(),
description: None,
variables: None
}],
paths: Paths::new(),
components: None,
security: Vec::new(),
tags: Vec::new(),
external_docs: None
})
}
/// 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
{
match self.0.paths.swap_remove(path) {
Some(Item(item)) => item,
_ => PathItem::default()
}
}
fn add_path<Path : ToString>(&mut self, path : Path, item : PathItem)
{
self.0.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);
match &mut self.0.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()));
self.0.components = Some(comp);
}
};
}
fn add_schema_dependencies(&mut self, dependencies : &mut IndexMap<String, OpenapiSchema>)
{
let keys : Vec<String> = 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<T : OpenapiType>(&mut self) -> ReferenceOr<Schema>
{
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);
impl OpenapiHandler
{
fn new(openapi : &OpenapiBuilder) -> Self
{
Self(openapi.0.clone())
}
}
impl NewHandler for OpenapiHandler
{
type Instance = Self;
fn new_handler(&self) -> gotham::error::Result<Self::Instance>
{
Ok(self.clone())
}
}
#[cfg(feature = "auth")]
const SECURITY_NAME : &'static str = "authToken";
#[cfg(feature = "auth")]
fn get_security(state : &mut State) -> IndexMap<String, ReferenceOr<SecurityScheme>>
{
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<String, ReferenceOr<SecurityScheme>> = 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<SecurityRequirement>, IndexMap<String, ReferenceOr<SecurityScheme>>)
{
Default::default()
}
impl Handler for OpenapiHandler
{
fn handle(self, mut state : State) -> Box<HandlerFuture>
{
let mut openapi = self.0;
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, hyper::StatusCode::OK, APPLICATION_JSON, body);
Box::new(ok((state, res)))
},
Err(e) => {
error!("Unable to handle OpenAPI request due to error: {}", e);
let res = create_response(&state, hyper::StatusCode::INTERNAL_SERVER_ERROR, TEXT_PLAIN, "");
Box::new(ok((state, res)))
}
}
}
}
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
pub trait GetOpenapi
{
fn get_openapi(&mut self, path : &str);
}
fn schema_to_content(types : Vec<Mime>, schema : ReferenceOr<Schema>) -> IndexMap<String, MediaType>
{
let mut content : IndexMap<String, MediaType> = IndexMap::new();
for ty in types
{
content.insert(ty.to_string(), MediaType {
schema: Some(schema.clone()),
example: None,
examples: IndexMap::new(),
encoding: IndexMap::new()
});
}
content
}
#[derive(Default)]
struct OperationParams<'a>
{
path_params : Vec<&'a str>,
query_params : Option<OpenapiSchema>
}
impl<'a> OperationParams<'a>
{
fn new(path_params : Vec<&'a str>, query_params : Option<OpenapiSchema>) -> Self
{
Self { path_params, query_params }
}
fn from_path_params(path_params : Vec<&'a str>) -> Self
{
Self::new(path_params, None)
}
fn from_query_params(query_params : OpenapiSchema) -> Self
{
Self::new(Vec::new(), Some(query_params))
}
fn add_path_params(&self, params : &mut Vec<ReferenceOr<Parameter>>)
{
for param in &self.path_params
{
params.push(Item(Parameter::Path {
parameter_data: ParameterData {
name: param.to_string(),
description: None,
required: true,
deprecated: None,
format: ParameterSchemaOrContent::Schema(Item(String::schema().into_schema())),
example: None,
examples: IndexMap::new()
},
style: Default::default(),
}));
}
}
fn add_query_params(self, params : &mut Vec<ReferenceOr<Parameter>>)
{
let query_params = match self.query_params {
Some(qp) => qp.schema,
None => return
};
let query_params = match query_params {
SchemaKind::Type(Type::Object(ty)) => ty,
_ => panic!("Query Parameters needs to be a plain struct")
};
for (name, schema) in query_params.properties
{
let required = query_params.required.contains(&name);
params.push(Item(Parameter::Query {
parameter_data: ParameterData {
name,
description: None,
required,
deprecated: None,
format: ParameterSchemaOrContent::Schema(schema.unbox()),
example: None,
examples: IndexMap::new()
},
allow_reserved: false,
style: Default::default(),
allow_empty_value: None
}))
}
}
fn into_params(self) -> Vec<ReferenceOr<Parameter>>
{
let mut params : Vec<ReferenceOr<Parameter>> = Vec::new();
self.add_path_params(&mut params);
self.add_query_params(&mut params);
params
}
}
fn new_operation(
default_status : hyper::StatusCode,
accepted_types : Option<Vec<Mime>>,
schema : ReferenceOr<Schema>,
params : OperationParams,
body_schema : Option<ReferenceOr<Schema>>,
supported_types : Option<Vec<Mime>>,
requires_auth : bool
) -> Operation
{
let content = schema_to_content(accepted_types.unwrap_or_default(), schema);
let mut responses : IndexMap<StatusCode, ReferenceOr<Response>> = IndexMap::new();
responses.insert(StatusCode::Code(default_status.as_u16()), Item(Response {
description: default_status.canonical_reason().map(|d| d.to_string()).unwrap_or_default(),
headers: IndexMap::new(),
content,
links: IndexMap::new()
}));
let request_body = body_schema.map(|schema| Item(OARequestBody {
description: None,
content: schema_to_content(supported_types.unwrap_or_default(), schema),
required: true
}));
let mut security = Vec::new();
if requires_auth
{
let mut sec = IndexMap::new();
sec.insert(SECURITY_NAME.to_owned(), Vec::new());
security.push(sec);
}
Operation {
tags: Vec::new(),
summary: None,
description: None,
external_documentation: None,
operation_id: None, // TODO
parameters: params.into_params(),
request_body,
responses: Responses {
default: None,
responses
},
deprecated: false,
security,
servers: Vec::new()
}
}
struct OpenapiRouteBuilder<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : Send + Sync + 'static
{
node_builder : &'a mut Node,
pipeline_chain : C,
pipelines : PipelineSet<P>
}
impl<'a, C, P> OpenapiRouteBuilder<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn new<D>(router : &'a mut D) -> Self
where
D : DrawRoutes<C, P> + 'a
{
let (node_builder, pipeline_chain, pipelines) = router.component_refs();
Self {
node_builder,
pipeline_chain: *pipeline_chain,
pipelines: pipelines.clone()
}
}
}
impl<'a, C, P> DrawRoutes<C, P> for OpenapiRouteBuilder<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn component_refs(&mut self) -> (&mut Node, &mut C, &PipelineSet<P>)
{
(&mut self.node_builder, &mut self.pipeline_chain, &self.pipelines)
}
}
/**
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 OpenapiRouter<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : Send + Sync + 'static
{
builder : OpenapiRouteBuilder<'a, C, P>,
openapi : OpenapiBuilder
}
impl<'a, C, P> OpenapiRouter<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
pub fn new<D>(router : &'a mut D, openapi : OpenapiBuilder) -> Self
where
D : DrawRoutes<C, P>
{
Self {
builder: OpenapiRouteBuilder::new(router),
openapi
}
}
}
impl<'a, C, P> GetOpenapi for OpenapiRouter<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn get_openapi(&mut self, path : &str)
{
self.builder.get(path).to_new_handler(OpenapiHandler::new(&self.openapi));
}
}
impl<'a, C, P> DrawResources<C, P> for OpenapiRouter<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn resource<'b, R : Resource>(&'b mut self, path : &'b str)
{
R::setup(OpenapiResourceSetup::new(self, path));
}
}
struct OpenapiResourceSetup<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : Send + Sync + 'static
{
builder : OpenapiRouteBuilder<'a, C, P>,
openapi : &'a mut OpenapiBuilder,
path : &'a str
}
impl<'a, C, P> OpenapiResourceSetup<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : Send + Sync + 'static
{
fn new(router : &'a mut OpenapiRouter<'a, C, P>, path : &'a str) -> Self
{
Self {
builder: &mut router.builder,
openapi: &mut router.openapi,
path
}
}
}
/*
impl<'a, C, P> DrawResourceRoutes for OpenapiResourceSetup<'a, C, P>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceReadAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read_all::<Handler, Res>()
}
fn read<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceRead<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).read::<Handler, ID, Res>()
}
fn search<Handler, Query, Res>(&mut self)
where
Query : ResourceType + DeserializeOwned + QueryStringExtractor<Body> + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceSearch<Query, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/search", &self.1);
let mut item = (self.0).1.remove_path(&self.1);
item.get = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_query_params(Query::schema()), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).search::<Handler, Query, Res>()
}
fn create<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceCreate<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.post = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).create::<Handler, Body, Res>()
}
fn update_all<Handler, Body, Res>(&mut self)
where
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdateAll<Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update_all::<Handler, Body, Res>()
}
fn update<Handler, ID, Body, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Body : RequestBody,
Res : ResourceResult,
Handler : ResourceUpdate<ID, Body, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let body_schema = (self.0).1.add_schema::<Body>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.put = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), Some(body_schema), Body::supported_types(), Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).update::<Handler, ID, Body, Res>()
}
fn delete_all<Handler, Res>(&mut self)
where
Res : ResourceResult,
Handler : ResourceDeleteAll<Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::default(), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete_all::<Handler, Res>()
}
fn delete<Handler, ID, Res>(&mut self)
where
ID : DeserializeOwned + Clone + RefUnwindSafe + Send + Sync + 'static,
Res : ResourceResult,
Handler : ResourceDelete<ID, Res>
{
let schema = (self.0).1.add_schema::<Res>();
let path = format!("/{}/{{id}}", &self.1);
let mut item = (self.0).1.remove_path(&path);
item.delete = Some(new_operation(Res::default_status(), Res::accepted_types(), schema, OperationParams::from_path_params(vec!["id"]), None, None, Res::requires_auth()));
(self.0).1.add_path(path, item);
(&mut *(self.0).0, self.1.to_string()).delete::<Handler, ID, Res>()
}
}
#[cfg(test)]
mod test
{
use crate::ResourceResult;
use super::*;
#[derive(OpenapiType)]
#[allow(dead_code)]
struct QueryParams
{
id : isize
}
#[test]
fn params_empty()
{
let op_params = OperationParams::default();
let params = op_params.into_params();
assert!(params.is_empty());
}
#[test]
fn params_from_path_params()
{
let name = "id";
let op_params = OperationParams::from_path_params(vec![name]);
let params = op_params.into_params();
let json = serde_json::to_string(&params).unwrap();
assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}}]"#, name));
}
#[test]
fn params_from_query_params()
{
let op_params = OperationParams::from_query_params(QueryParams::schema());
let params = op_params.into_params();
let json = serde_json::to_string(&params).unwrap();
assert_eq!(json, r#"[{"in":"query","name":"id","required":true,"schema":{"type":"integer"},"style":"form"}]"#);
}
#[test]
fn params_both()
{
let name = "id";
let op_params = OperationParams::new(vec![name], Some(QueryParams::schema()));
let params = op_params.into_params();
let json = serde_json::to_string(&params).unwrap();
assert_eq!(json, format!(r#"[{{"in":"path","name":"{}","required":true,"schema":{{"type":"string"}},"style":"simple"}},{{"in":"query","name":"id","required":true,"schema":{{"type":"integer"}},"style":"form"}}]"#, name));
}
#[test]
fn no_content_schema_to_content()
{
let types = NoContent::accepted_types();
let schema = <NoContent as OpenapiType>::schema();
let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema()));
assert!(content.is_empty());
}
#[test]
fn raw_schema_to_content()
{
let types = Raw::<&str>::accepted_types();
let schema = <Raw<&str> as OpenapiType>::schema();
let content = schema_to_content(types.unwrap_or_default(), Item(schema.into_schema()));
assert_eq!(content.len(), 1);
let json = serde_json::to_string(&content.values().nth(0).unwrap()).unwrap();
assert_eq!(json, r#"{"schema":{"type":"string","format":"binary"}}"#);
}
}
*/

View file

@ -0,0 +1,312 @@
#[cfg(feature = "chrono")]
use chrono::{
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
};
use indexmap::IndexMap;
use openapiv3::{
ArrayType, IntegerType, NumberType, ObjectType, ReferenceOr::Item, ReferenceOr::Reference, Schema,
SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
};
#[cfg(feature = "uuid")]
use uuid::Uuid;
use std::collections::{BTreeSet, HashSet};
/**
This struct needs to be available for every type that can be part of an OpenAPI Spec. It is
already implemented for primitive types, String, Vec, Option and the like. To have it available
for your type, simply derive from [`OpenapiType`].
[`OpenapiType`]: trait.OpenapiType.html
*/
#[derive(Debug, Clone, PartialEq)]
pub struct OpenapiSchema
{
/// The name of this schema. If it is None, the schema will be inlined.
pub name : Option<String>,
/// Whether this particular schema is nullable. Note that there is no guarantee that this will
/// make it into the final specification, it might just be interpreted as a hint to make it
/// an optional parameter.
pub nullable : bool,
/// The actual OpenAPI schema.
pub schema : SchemaKind,
/// Other schemas that this schema depends on. They will be included in the final OpenAPI Spec
/// along with this schema.
pub dependencies : IndexMap<String, OpenapiSchema>
}
impl OpenapiSchema
{
/// Create a new schema that has no name.
pub fn new(schema : SchemaKind) -> Self
{
Self {
name: None,
nullable: false,
schema,
dependencies: IndexMap::new()
}
}
/// Convert this schema to an `openapiv3::Schema` that can be serialized to the OpenAPI Spec.
pub fn into_schema(self) -> Schema
{
Schema {
schema_data: SchemaData {
nullable: self.nullable,
title: self.name,
..Default::default()
},
schema_kind: self.schema
}
}
}
/**
This trait needs to be implemented by every type that is being used in the OpenAPI Spec. It gives
access to the [`OpenapiSchema`] of this type. It is provided for primitive types, String and the
like. For use on your own types, there is a derive macro:
```
# #[macro_use] extern crate gotham_restful_derive;
#
#[derive(OpenapiType)]
struct MyResponse {
message: String
}
```
[`OpenapiSchema`]: struct.OpenapiSchema.html
*/
pub trait OpenapiType
{
fn schema() -> OpenapiSchema;
}
impl OpenapiType for ()
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType::default())))
}
}
impl OpenapiType for bool
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Boolean{}))
}
}
macro_rules! int_types {
($($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
}
}
)*};
(unsigned $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
minimum: Some(0),
..Default::default()
})))
}
}
)*};
(bits = $bits:expr, $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
..Default::default()
})))
}
}
)*};
(unsigned bits = $bits:expr, $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
format: VariantOrUnknownOrEmpty::Unknown(format!("int{}", $bits)),
minimum: Some(0),
..Default::default()
})))
}
}
)*};
}
int_types!(isize);
int_types!(unsigned usize);
int_types!(bits = 8, i8);
int_types!(unsigned bits = 8, u8);
int_types!(bits = 16, i16);
int_types!(unsigned bits = 16, u16);
int_types!(bits = 32, i32);
int_types!(unsigned bits = 32, u32);
int_types!(bits = 64, i64);
int_types!(unsigned bits = 64, u64);
int_types!(bits = 128, i128);
int_types!(unsigned bits = 128, u128);
macro_rules! num_types {
($($num_ty:ty),*) => {$(
impl OpenapiType for $num_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType::default())))
}
}
)*}
}
num_types!(f32, f64);
macro_rules! str_types {
($($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType::default())))
}
}
)*};
(format = $format:ident, $($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
use openapiv3::StringFormat;
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Item(StringFormat::$format),
..Default::default()
})))
}
}
)*};
(format_str = $format:expr, $($str_ty:ty),*) => {$(
impl OpenapiType for $str_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
format: VariantOrUnknownOrEmpty::Unknown($format.to_string()),
..Default::default()
})))
}
}
)*};
}
str_types!(String, &str);
impl<T : OpenapiType> OpenapiType for Option<T>
{
fn schema() -> OpenapiSchema
{
let schema = T::schema();
let mut dependencies = schema.dependencies.clone();
let schema = match schema.name.clone() {
Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
dependencies.insert(name, schema);
SchemaKind::AllOf { all_of: vec![reference] }
},
None => schema.schema
};
OpenapiSchema {
nullable: true,
name: None,
schema,
dependencies
}
}
}
impl<T : OpenapiType> OpenapiType for Vec<T>
{
fn schema() -> OpenapiSchema
{
let schema = T::schema();
let mut dependencies = schema.dependencies.clone();
let items = match schema.name.clone()
{
Some(name) => {
let reference = Reference { reference: format!("#/components/schemas/{}", name) };
dependencies.insert(name, schema);
reference
},
None => Item(Box::new(schema.into_schema()))
};
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Array(ArrayType {
items,
min_items: None,
max_items: None,
unique_items: false
})),
dependencies
}
}
}
impl<T : OpenapiType> OpenapiType for BTreeSet<T>
{
fn schema() -> OpenapiSchema
{
<Vec<T> as OpenapiType>::schema()
}
}
impl<T : OpenapiType> OpenapiType for HashSet<T>
{
fn schema() -> OpenapiSchema
{
<Vec<T> as OpenapiType>::schema()
}
}
#[cfg(feature = "chrono")]
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate);
#[cfg(feature = "chrono")]
str_types!(format = DateTime, DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime);
#[cfg(feature = "uuid")]
str_types!(format_str = "uuid", Uuid);
impl OpenapiType for serde_json::Value
{
fn schema() -> OpenapiSchema
{
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
}
}
}