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

merge workspace and main crate

This commit is contained in:
Dominic 2020-05-05 23:18:05 +02:00
parent 52679ad29d
commit 5587ded60d
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
45 changed files with 58 additions and 67 deletions

162
src/openapi/builder.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::{OpenapiType, OpenapiSchema};
use indexmap::IndexMap;
use openapiv3::{
Components, OpenAPI, PathItem, ReferenceOr, ReferenceOr::Item, ReferenceOr::Reference, Schema,
Server
};
use std::sync::{Arc, RwLock};
#[derive(Clone, Debug)]
pub struct OpenapiInfo
{
pub title : String,
pub version : String,
pub urls : Vec<String>
}
#[derive(Clone, Debug)]
pub struct OpenapiBuilder
{
pub openapi : Arc<RwLock<OpenAPI>>
}
impl OpenapiBuilder
{
pub fn new(info : OpenapiInfo) -> Self
{
Self {
openapi: Arc::new(RwLock::new(OpenAPI {
openapi: "3.0.2".to_string(),
info: openapiv3::Info {
title: info.title,
version: info.version,
..Default::default()
},
servers: info.urls.into_iter()
.map(|url| Server { url, ..Default::default() })
.collect(),
..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<Path : ToString>(&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<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);
}
}
}
pub 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())
}
}
}
}
#[cfg(test)]
#[allow(dead_code)]
mod test
{
use super::*;
#[derive(OpenapiType)]
struct Message
{
msg : String
}
#[derive(OpenapiType)]
struct Messages
{
msgs : Vec<Message>
}
fn info() -> OpenapiInfo
{
OpenapiInfo {
title: "TEST CASE".to_owned(),
version: "1.2.3".to_owned(),
urls: vec!["http://localhost:1234".to_owned(), "https://example.org".to_owned()]
}
}
fn openapi(builder : OpenapiBuilder) -> OpenAPI
{
Arc::try_unwrap(builder.openapi).unwrap().into_inner().unwrap()
}
#[test]
fn new_builder()
{
let info = info();
let builder = OpenapiBuilder::new(info.clone());
let openapi = openapi(builder);
assert_eq!(info.title, openapi.info.title);
assert_eq!(info.version, openapi.info.version);
assert_eq!(info.urls.len(), openapi.servers.len());
}
#[test]
fn add_schema()
{
let mut builder = OpenapiBuilder::new(info());
builder.add_schema::<Option<Messages>>();
let openapi = openapi(builder);
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Message"] , ReferenceOr::Item(Message ::schema().into_schema()));
assert_eq!(openapi.components.clone().unwrap_or_default().schemas["Messages"], ReferenceOr::Item(Messages::schema().into_schema()));
}
}

110
src/openapi/handler.rs Normal file
View file

@ -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<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) -> Result<Self>
{
Ok(self.clone())
}
}
#[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) -> 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, 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()
}
}
}
}

8
src/openapi/mod.rs Normal file
View file

@ -0,0 +1,8 @@
const SECURITY_NAME : &str = "authToken";
pub mod builder;
pub mod handler;
pub mod operation;
pub mod router;
pub mod types;

216
src/openapi/operation.rs Normal file
View file

@ -0,0 +1,216 @@
use crate::{
resource::*,
result::*,
OpenapiSchema,
RequestBody
};
use super::SECURITY_NAME;
use indexmap::IndexMap;
use mime::Mime;
use openapiv3::{
MediaType, Operation, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr,
ReferenceOr::Item, RequestBody as OARequestBody, Response, Responses, Schema, SchemaKind,
StatusCode, Type
};
#[derive(Default)]
struct OperationParams<'a>
{
path_params : Vec<(&'a str, ReferenceOr<Schema>)>,
query_params : Option<OpenapiSchema>
}
impl<'a> OperationParams<'a>
{
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).0.to_string(),
description: None,
required: true,
deprecated: None,
format: ParameterSchemaOrContent::Schema((*param).1.clone()),
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
}
}
pub struct OperationDescription<'a>
{
operation_id : Option<String>,
default_status : crate::StatusCode,
accepted_types : Option<Vec<Mime>>,
schema : ReferenceOr<Schema>,
params : OperationParams<'a>,
body_schema : Option<ReferenceOr<Schema>>,
supported_types : Option<Vec<Mime>>,
requires_auth : bool
}
impl<'a> OperationDescription<'a>
{
pub fn new<Handler : ResourceMethod>(schema : ReferenceOr<Schema>) -> Self
{
Self {
operation_id: Handler::operation_id(),
default_status: Handler::Res::default_status(),
accepted_types: Handler::Res::accepted_types(),
schema,
params: Default::default(),
body_schema: None,
supported_types: None,
requires_auth: Handler::wants_auth()
}
}
pub fn add_path_param(mut self, name : &'a str, schema : ReferenceOr<Schema>) -> Self
{
self.params.path_params.push((name, schema));
self
}
pub fn with_query_params(mut self, params : OpenapiSchema) -> Self
{
self.params.query_params = Some(params);
self
}
pub fn with_body<Body : RequestBody>(mut self, schema : ReferenceOr<Schema>) -> Self
{
self.body_schema = Some(schema);
self.supported_types = Body::supported_types();
self
}
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()),
..Default::default()
});
}
content
}
pub fn into_operation(self) -> Operation
{
// this is unfortunately neccessary to prevent rust from complaining about partially moving self
let (operation_id, default_status, accepted_types, schema, params, body_schema, supported_types, requires_auth) = (
self.operation_id, self.default_status, self.accepted_types, self.schema, self.params, self.body_schema, self.supported_types, self.requires_auth);
let content = Self::schema_to_content(accepted_types.or_all_types(), 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(),
content,
..Default::default()
}));
let request_body = body_schema.map(|schema| Item(OARequestBody {
description: None,
content: Self::schema_to_content(supported_types.or_all_types(), 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(),
operation_id,
parameters: params.into_params(),
request_body,
responses: Responses {
default: None,
responses
},
deprecated: false,
security,
..Default::default()
}
}
}
#[cfg(test)]
mod test
{
use crate::{OpenapiType, ResourceResult};
use super::*;
#[test]
fn no_content_schema_to_content()
{
let types = NoContent::accepted_types();
let schema = <NoContent as OpenapiType>::schema();
let content = OperationDescription::schema_to_content(types.or_all_types(), 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 = OperationDescription::schema_to_content(types.or_all_types(), 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"}}"#);
}
}

195
src/openapi/router.rs Normal file
View file

@ -0,0 +1,195 @@
use crate::{
resource::*,
routing::*,
OpenapiType,
};
use super::{builder::OpenapiBuilder, handler::OpenapiHandler, operation::OperationDescription};
use gotham::{
pipeline::chain::PipelineHandleChain,
router::builder::*
};
use std::panic::RefUnwindSafe;
/// This trait adds the `get_openapi` method to an OpenAPI-aware router.
pub trait GetOpenapi
{
fn get_openapi(&mut self, path : &str);
}
#[derive(Debug)]
pub struct OpenapiRouter<'a, D>
{
pub(crate) router : &'a mut D,
pub(crate) scope : Option<&'a str>,
pub(crate) openapi_builder : &'a mut OpenapiBuilder
}
macro_rules! implOpenapiRouter {
($implType:ident) => {
impl<'a, 'b, C, P> OpenapiRouter<'a, $implType<'b, C, P>>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
pub fn scope<F>(&mut self, path : &str, callback : F)
where
F : FnOnce(&mut OpenapiRouter<'_, ScopeBuilder<'_, C, P>>)
{
let mut openapi_builder = self.openapi_builder.clone();
let new_scope = self.scope.map(|scope| format!("{}/{}", scope, path).replace("//", "/"));
self.router.scope(path, |router| {
let mut router = OpenapiRouter {
router,
scope: Some(new_scope.as_ref().map(String::as_ref).unwrap_or(path)),
openapi_builder: &mut openapi_builder
};
callback(&mut router);
});
}
}
impl<'a, 'b, C, P> GetOpenapi for OpenapiRouter<'a, $implType<'b, C, P>>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn get_openapi(&mut self, path : &str)
{
self.router.get(path).to_new_handler(OpenapiHandler::new(self.openapi_builder.openapi.clone()));
}
}
impl<'a, 'b, C, P> DrawResources for OpenapiRouter<'a, $implType<'b, C, P>>
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn resource<R : Resource>(&mut self, path : &str)
{
R::setup((self, path));
}
}
impl<'a, 'b, C, P> DrawResourceRoutes for (&mut OpenapiRouter<'a, $implType<'b, C, P>>, &str)
where
C : PipelineHandleChain<P> + Copy + Send + Sync + 'static,
P : RefUnwindSafe + Send + Sync + 'static
{
fn read_all<Handler : ResourceReadAll>(&mut self)
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).read_all::<Handler>()
}
fn read<Handler : ResourceRead>(&mut self)
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).read::<Handler>()
}
fn search<Handler : ResourceSearch>(&mut self)
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}/search", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.get = Some(OperationDescription::new::<Handler>(schema).with_query_params(Handler::Query::schema()).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).search::<Handler>()
}
fn create<Handler : ResourceCreate>(&mut self)
where
Handler::Res : 'static,
Handler::Body : 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.post = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).create::<Handler>()
}
fn change_all<Handler : ResourceChangeAll>(&mut self)
where
Handler::Res : 'static,
Handler::Body : 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(OperationDescription::new::<Handler>(schema).with_body::<Handler::Body>(body_schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change_all::<Handler>()
}
fn change<Handler : ResourceChange>(&mut self)
where
Handler::Res : 'static,
Handler::Body : 'static
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let body_schema = (self.0).openapi_builder.add_schema::<Handler::Body>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.put = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).with_body::<Handler::Body>(body_schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).change::<Handler>()
}
fn remove_all<Handler : ResourceRemoveAll>(&mut self)
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let path = format!("{}/{}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.delete = Some(OperationDescription::new::<Handler>(schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).remove_all::<Handler>()
}
fn remove<Handler : ResourceRemove>(&mut self)
{
let schema = (self.0).openapi_builder.add_schema::<Handler::Res>();
let id_schema = (self.0).openapi_builder.add_schema::<Handler::ID>();
let path = format!("{}/{}/{{id}}", self.0.scope.unwrap_or_default(), self.1);
let mut item = (self.0).openapi_builder.remove_path(&path);
item.delete = Some(OperationDescription::new::<Handler>(schema).add_path_param("id", id_schema).into_operation());
(self.0).openapi_builder.add_path(path, item);
(&mut *(self.0).router, self.1).remove::<Handler>()
}
}
}
}
implOpenapiRouter!(RouterBuilder);
implOpenapiRouter!(ScopeBuilder);

387
src/openapi/types.rs Normal file
View file

@ -0,0 +1,387 @@
#[cfg(feature = "chrono")]
use chrono::{
Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc
};
use indexmap::IndexMap;
use openapiv3::{
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr::Item,
ReferenceOr::Reference, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
};
#[cfg(feature = "uuid")]
use uuid::Uuid;
use std::{
collections::{BTreeSet, HashSet},
hash::BuildHasher
};
/**
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 {
additional_properties: Some(AdditionalProperties::Any(false)),
..Default::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 = $num_fmt:ident),*) => {$(
impl OpenapiType for $num_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
format: VariantOrUnknownOrEmpty::Item(NumberFormat::$num_fmt),
..Default::default()
})))
}
}
)*}
}
num_types!(f32 = Float, f64 = Double);
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);
#[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<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, S : BuildHasher> OpenapiType for HashSet<T, S>
{
fn schema() -> OpenapiSchema
{
<Vec<T> as OpenapiType>::schema()
}
}
impl OpenapiType for serde_json::Value
{
fn schema() -> OpenapiSchema
{
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
}
}
}
#[cfg(test)]
mod test
{
use super::*;
use serde_json::Value;
type Unit = ();
macro_rules! assert_schema {
($ty:ident $(<$generic:ident>)* => $json:expr) => {
paste::item! {
#[test]
fn [<test_schema_ $ty:snake $(_ $generic:snake)*>]()
{
let schema = <$ty $(<$generic>)* as OpenapiType>::schema().into_schema();
let schema_json = serde_json::to_string(&schema).expect(&format!("Unable to serialize schema for {}", stringify!($ty)));
assert_eq!(schema_json, $json);
}
}
};
}
assert_schema!(Unit => r#"{"type":"object","additionalProperties":false}"#);
assert_schema!(bool => r#"{"type":"boolean"}"#);
assert_schema!(isize => r#"{"type":"integer"}"#);
assert_schema!(usize => r#"{"type":"integer","minimum":0}"#);
assert_schema!(i8 => r#"{"type":"integer","format":"int8"}"#);
assert_schema!(u8 => r#"{"type":"integer","format":"int8","minimum":0}"#);
assert_schema!(i16 => r#"{"type":"integer","format":"int16"}"#);
assert_schema!(u16 => r#"{"type":"integer","format":"int16","minimum":0}"#);
assert_schema!(i32 => r#"{"type":"integer","format":"int32"}"#);
assert_schema!(u32 => r#"{"type":"integer","format":"int32","minimum":0}"#);
assert_schema!(i64 => r#"{"type":"integer","format":"int64"}"#);
assert_schema!(u64 => r#"{"type":"integer","format":"int64","minimum":0}"#);
assert_schema!(i128 => r#"{"type":"integer","format":"int128"}"#);
assert_schema!(u128 => r#"{"type":"integer","format":"int128","minimum":0}"#);
assert_schema!(f32 => r#"{"type":"number","format":"float"}"#);
assert_schema!(f64 => r#"{"type":"number","format":"double"}"#);
assert_schema!(String => r#"{"type":"string"}"#);
#[cfg(feature = "chrono")]
assert_schema!(Date<FixedOffset> => r#"{"type":"string","format":"date"}"#);
#[cfg(feature = "chrono")]
assert_schema!(Date<Local> => r#"{"type":"string","format":"date"}"#);
#[cfg(feature = "chrono")]
assert_schema!(Date<Utc> => r#"{"type":"string","format":"date"}"#);
#[cfg(feature = "chrono")]
assert_schema!(NaiveDate => r#"{"type":"string","format":"date"}"#);
#[cfg(feature = "chrono")]
assert_schema!(DateTime<FixedOffset> => r#"{"type":"string","format":"date-time"}"#);
#[cfg(feature = "chrono")]
assert_schema!(DateTime<Local> => r#"{"type":"string","format":"date-time"}"#);
#[cfg(feature = "chrono")]
assert_schema!(DateTime<Utc> => r#"{"type":"string","format":"date-time"}"#);
#[cfg(feature = "chrono")]
assert_schema!(NaiveDateTime => r#"{"type":"string","format":"date-time"}"#);
#[cfg(feature = "uuid")]
assert_schema!(Uuid => r#"{"type":"string","format":"uuid"}"#);
assert_schema!(Option<String> => r#"{"nullable":true,"type":"string"}"#);
assert_schema!(Vec<String> => r#"{"type":"array","items":{"type":"string"}}"#);
assert_schema!(Value => r#"{"nullable":true}"#);
}