1
0
Fork 0
mirror of https://gitlab.com/msrd0/gotham-restful.git synced 2025-04-18 14:04:38 +00:00

redo and test openapi type implementations

This commit is contained in:
Dominic 2021-03-09 17:07:16 +01:00
parent 2a35e044db
commit eecd192458
Signed by: msrd0
GPG key ID: DCC8C247452E98F9
4 changed files with 413 additions and 291 deletions

View file

@ -17,6 +17,10 @@ openapi_type_derive = "0.1.0-dev"
openapiv3 = "=0.3.2" openapiv3 = "=0.3.2"
serde_json = "1.0" serde_json = "1.0"
# optional dependencies / features
chrono = { version = "0.4.19", optional = true }
uuid = { version = "0.8.2" , optional = true }
[dev-dependencies] [dev-dependencies]
paste = "1.0" paste = "1.0"
serde = "1.0" serde = "1.0"

View file

@ -1,321 +1,224 @@
use crate::{OpenapiSchema, OpenapiType}; use crate::{OpenapiSchema, OpenapiType};
use indexmap::IndexMap; #[cfg(feature = "chrono")]
use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime};
use indexmap::{IndexMap, IndexSet};
use openapiv3::{ use openapiv3::{
AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind,
Type, VariantOrUnknownOrEmpty StringFormat, StringType, Type, VariantOrUnknownOrEmpty
}; };
use serde_json::Value;
use std::{ use std::{
collections::{BTreeSet, HashMap, HashSet}, collections::{BTreeMap, BTreeSet, HashMap, HashSet},
hash::BuildHasher, hash::BuildHasher,
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
}; };
#[cfg(feature = "uuid")]
use uuid::Uuid;
impl OpenapiType for () { macro_rules! impl_openapi_type {
fn schema() -> OpenapiSchema { ($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => {
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { $(
additional_properties: Some(AdditionalProperties::Any(false)), impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* {
..Default::default() fn schema() -> OpenapiSchema {
}))) $schema
}
}
)*
};
}
type Unit = ();
impl_openapi_type!(Unit => {
OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType {
additional_properties: Some(AdditionalProperties::Any(false)),
..Default::default()
})))
});
impl_openapi_type!(Value => {
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
} }
});
impl_openapi_type!(bool => OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})));
#[inline]
fn int_schema(minimum: Option<i64>, bits: Option<i64>) -> OpenapiSchema {
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
minimum,
format: bits
.map(|bits| VariantOrUnknownOrEmpty::Unknown(format!("int{}", bits)))
.unwrap_or(VariantOrUnknownOrEmpty::Empty),
..Default::default()
})))
} }
impl OpenapiType for bool { impl_openapi_type!(isize => int_schema(None, None));
fn schema() -> OpenapiSchema { impl_openapi_type!(i8 => int_schema(None, Some(8)));
OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) impl_openapi_type!(i16 => int_schema(None, Some(16)));
} impl_openapi_type!(i32 => int_schema(None, Some(32)));
impl_openapi_type!(i64 => int_schema(None, Some(64)));
impl_openapi_type!(i128 => int_schema(None, Some(128)));
impl_openapi_type!(usize => int_schema(Some(0), None));
impl_openapi_type!(u8 => int_schema(Some(0), Some(8)));
impl_openapi_type!(u16 => int_schema(Some(0), Some(16)));
impl_openapi_type!(u32 => int_schema(Some(0), Some(32)));
impl_openapi_type!(u64 => int_schema(Some(0), Some(64)));
impl_openapi_type!(u128 => int_schema(Some(0), Some(128)));
impl_openapi_type!(NonZeroUsize => int_schema(Some(1), None));
impl_openapi_type!(NonZeroU8 => int_schema(Some(1), Some(8)));
impl_openapi_type!(NonZeroU16 => int_schema(Some(1), Some(16)));
impl_openapi_type!(NonZeroU32 => int_schema(Some(1), Some(32)));
impl_openapi_type!(NonZeroU64 => int_schema(Some(1), Some(64)));
impl_openapi_type!(NonZeroU128 => int_schema(Some(1), Some(128)));
#[inline]
fn float_schema(format: NumberFormat) -> OpenapiSchema {
OpenapiSchema::new(SchemaKind::Type(Type::Number(NumberType {
format: VariantOrUnknownOrEmpty::Item(format),
..Default::default()
})))
} }
macro_rules! int_types { impl_openapi_type!(f32 => float_schema(NumberFormat::Float));
($($int_ty:ty),*) => {$( impl_openapi_type!(f64 => float_schema(NumberFormat::Double));
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default())))
}
}
)*};
(unsigned $($int_ty:ty),*) => {$( #[inline]
impl OpenapiType for $int_ty fn str_schema(format: VariantOrUnknownOrEmpty<StringFormat>) -> OpenapiSchema {
{ OpenapiSchema::new(SchemaKind::Type(Type::String(StringType {
fn schema() -> OpenapiSchema format,
{ ..Default::default()
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType { })))
minimum: Some(0),
..Default::default()
})))
}
}
)*};
(gtzero $($int_ty:ty),*) => {$(
impl OpenapiType for $int_ty
{
fn schema() -> OpenapiSchema
{
OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType {
minimum: Some(1),
..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()
})))
}
}
)*};
(gtzero 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(1),
..Default::default()
})))
}
}
)*};
} }
int_types!(isize); impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty));
int_types!(unsigned usize);
int_types!(gtzero NonZeroUsize);
int_types!(bits = 8, i8);
int_types!(unsigned bits = 8, u8);
int_types!(gtzero bits = 8, NonZeroU8);
int_types!(bits = 16, i16);
int_types!(unsigned bits = 16, u16);
int_types!(gtzero bits = 16, NonZeroU16);
int_types!(bits = 32, i32);
int_types!(unsigned bits = 32, u32);
int_types!(gtzero bits = 32, NonZeroU32);
int_types!(bits = 64, i64);
int_types!(unsigned bits = 64, u64);
int_types!(gtzero bits = 64, NonZeroU64);
int_types!(bits = 128, i128);
int_types!(unsigned bits = 128, u128);
int_types!(gtzero bits = 128, NonZeroU128);
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")] #[cfg(feature = "chrono")]
str_types!(format = Date, Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate); impl_openapi_type!(Date<T: TimeZone>, NaiveDate => {
str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date))
});
#[cfg(feature = "chrono")] #[cfg(feature = "chrono")]
str_types!( impl_openapi_type!(DateTime<T: TimeZone>, NaiveDateTime => {
format = DateTime, str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime))
DateTime<FixedOffset>, });
DateTime<Local>,
DateTime<Utc>,
NaiveDateTime
);
#[cfg(feature = "uuid")] #[cfg(feature = "uuid")]
str_types!(format_str = "uuid", Uuid); impl_openapi_type!(Uuid => {
str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned()))
});
impl<T: OpenapiType> OpenapiType for Option<T> { impl_openapi_type!(Option<T: OpenapiType> => {
fn schema() -> OpenapiSchema { let schema = T::schema();
let schema = T::schema(); let mut dependencies = schema.dependencies.clone();
let mut dependencies = schema.dependencies.clone(); let schema = match schema.name.clone() {
let schema = match schema.name.clone() { Some(name) => {
Some(name) => { let reference = ReferenceOr::Reference {
let reference = ReferenceOr::Reference { reference: format!("#/components/schemas/{}", name)
reference: format!("#/components/schemas/{}", name) };
}; dependencies.insert(name, schema);
dependencies.insert(name, schema); SchemaKind::AllOf { all_of: vec![reference] }
SchemaKind::AllOf { all_of: vec![reference] } },
}, None => schema.schema
None => schema.schema };
};
OpenapiSchema { OpenapiSchema {
nullable: true, nullable: true,
name: None, name: None,
schema, schema,
dependencies dependencies
} }
});
#[inline]
fn array_schema<T: OpenapiType>(unique_items: bool) -> OpenapiSchema {
let schema = T::schema();
let mut dependencies = schema.dependencies.clone();
let items = match schema.name.clone() {
Some(name) => {
let reference = ReferenceOr::Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
reference
},
None => ReferenceOr::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
})),
dependencies
} }
} }
impl<T: OpenapiType> OpenapiType for Vec<T> { impl_openapi_type!(Vec<T: OpenapiType> => array_schema::<T>(false));
fn schema() -> OpenapiSchema { impl_openapi_type!(BTreeSet<T: OpenapiType>, IndexSet<T: OpenapiType>, HashSet<T: OpenapiType, S: BuildHasher> => {
let schema = T::schema(); array_schema::<T>(true)
let mut dependencies = schema.dependencies.clone(); });
let items = match schema.name.clone() { #[inline]
Some(name) => { fn map_schema<K: OpenapiType, T: OpenapiType>() -> OpenapiSchema {
let reference = ReferenceOr::Reference { let key_schema = K::schema();
reference: format!("#/components/schemas/{}", name) let mut dependencies = key_schema.dependencies.clone();
};
dependencies.insert(name, schema);
reference
},
None => ReferenceOr::Item(Box::new(schema.into_schema()))
};
OpenapiSchema { let keys = match key_schema.name.clone() {
nullable: false, Some(name) => {
name: None, let reference = ReferenceOr::Reference {
schema: SchemaKind::Type(Type::Array(ArrayType { reference: format!("#/components/schemas/{}", name)
items, };
min_items: None, dependencies.insert(name, key_schema);
max_items: None, reference
unique_items: false },
})), None => ReferenceOr::Item(Box::new(key_schema.into_schema()))
dependencies };
}
let schema = T::schema();
dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone())));
let items = Box::new(match schema.name.clone() {
Some(name) => {
let reference = ReferenceOr::Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
reference
},
None => ReferenceOr::Item(schema.into_schema())
});
let mut properties = IndexMap::new();
properties.insert("default".to_owned(), keys);
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Object(ObjectType {
properties,
required: vec!["default".to_owned()],
additional_properties: Some(AdditionalProperties::Schema(items)),
..Default::default()
})),
dependencies
} }
} }
impl<T: OpenapiType> OpenapiType for BTreeSet<T> { impl_openapi_type!(
fn schema() -> OpenapiSchema { BTreeMap<K: OpenapiType, T: OpenapiType>,
<Vec<T> as OpenapiType>::schema() IndexMap<K: OpenapiType, T: OpenapiType>,
} HashMap<K: OpenapiType, T: OpenapiType, S: BuildHasher>
} => map_schema::<K, T>()
);
impl<T: OpenapiType, S: BuildHasher> OpenapiType for HashSet<T, S> {
fn schema() -> OpenapiSchema {
<Vec<T> as OpenapiType>::schema()
}
}
impl<K: OpenapiType, T: OpenapiType, S: BuildHasher> OpenapiType for HashMap<K, T, S> {
fn schema() -> OpenapiSchema {
let key_schema = K::schema();
let mut dependencies = key_schema.dependencies.clone();
let keys = match key_schema.name.clone() {
Some(name) => {
let reference = ReferenceOr::Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, key_schema);
reference
},
None => ReferenceOr::Item(Box::new(key_schema.into_schema()))
};
let schema = T::schema();
dependencies.extend(schema.dependencies.iter().map(|(k, v)| (k.clone(), v.clone())));
let items = Box::new(match schema.name.clone() {
Some(name) => {
let reference = ReferenceOr::Reference {
reference: format!("#/components/schemas/{}", name)
};
dependencies.insert(name, schema);
reference
},
None => ReferenceOr::Item(schema.into_schema())
});
let mut properties = IndexMap::new();
properties.insert("default".to_owned(), keys);
OpenapiSchema {
nullable: false,
name: None,
schema: SchemaKind::Type(Type::Object(ObjectType {
properties,
required: vec!["default".to_owned()],
additional_properties: Some(AdditionalProperties::Schema(items)),
..Default::default()
})),
dependencies
}
}
}
impl OpenapiType for serde_json::Value {
fn schema() -> OpenapiSchema {
OpenapiSchema {
nullable: true,
name: None,
schema: SchemaKind::Any(Default::default()),
dependencies: Default::default()
}
}
}

View file

@ -0,0 +1,216 @@
#[cfg(feature = "chrono")]
use chrono::{Date, DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, Utc};
use indexmap::{IndexMap, IndexSet};
use openapi_type::OpenapiType;
use serde_json::Value;
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize}
};
#[cfg(feature = "uuid")]
use uuid::Uuid;
macro_rules! test_type {
($($ty:ident $(<$($generic:ident),+>)*),* = $json:tt) => {
paste::paste! { $(
#[test]
fn [< $ty:lower $($(_ $generic:lower)+)* >]() {
let schema = <$ty $(<$($generic),+>)* as OpenapiType>::schema();
let schema = openapi_type::OpenapiSchema::into_schema(schema);
let schema_json = serde_json::to_value(&schema).unwrap();
let expected = serde_json::json!($json);
assert_eq!(schema_json, expected);
}
)* }
};
}
type Unit = ();
test_type!(Unit = {
"type": "object",
"additionalProperties": false
});
test_type!(Value = {
"nullable": true
});
test_type!(bool = {
"type": "boolean"
});
// ### integer types
test_type!(isize = {
"type": "integer"
});
test_type!(usize = {
"type": "integer",
"minimum": 0
});
test_type!(i8 = {
"type": "integer",
"format": "int8"
});
test_type!(u8 = {
"type": "integer",
"format": "int8",
"minimum": 0
});
test_type!(i16 = {
"type": "integer",
"format": "int16"
});
test_type!(u16 = {
"type": "integer",
"format": "int16",
"minimum": 0
});
test_type!(i32 = {
"type": "integer",
"format": "int32"
});
test_type!(u32 = {
"type": "integer",
"format": "int32",
"minimum": 0
});
test_type!(i64 = {
"type": "integer",
"format": "int64"
});
test_type!(u64 = {
"type": "integer",
"format": "int64",
"minimum": 0
});
test_type!(i128 = {
"type": "integer",
"format": "int128"
});
test_type!(u128 = {
"type": "integer",
"format": "int128",
"minimum": 0
});
// ### non-zero integer types
test_type!(NonZeroUsize = {
"type": "integer",
"minimum": 1
});
test_type!(NonZeroU8 = {
"type": "integer",
"format": "int8",
"minimum": 1
});
test_type!(NonZeroU16 = {
"type": "integer",
"format": "int16",
"minimum": 1
});
test_type!(NonZeroU32 = {
"type": "integer",
"format": "int32",
"minimum": 1
});
test_type!(NonZeroU64 = {
"type": "integer",
"format": "int64",
"minimum": 1
});
test_type!(NonZeroU128 = {
"type": "integer",
"format": "int128",
"minimum": 1
});
// ### floats
test_type!(f32 = {
"type": "number",
"format": "float"
});
test_type!(f64 = {
"type": "number",
"format": "double"
});
// ### string
test_type!(String = {
"type": "string"
});
#[cfg(feature = "uuid")]
test_type!(Uuid = {
"type": "string",
"format": "uuid"
});
// ### date/time
#[cfg(feature = "chrono")]
test_type!(Date<FixedOffset>, Date<Local>, Date<Utc>, NaiveDate = {
"type": "string",
"format": "date"
});
#[cfg(feature = "chrono")]
test_type!(DateTime<FixedOffset>, DateTime<Local>, DateTime<Utc>, NaiveDateTime = {
"type": "string",
"format": "date-time"
});
// ### some std types
test_type!(Option<String> = {
"type": "string",
"nullable": true
});
test_type!(Vec<String> = {
"type": "array",
"items": {
"type": "string"
}
});
test_type!(BTreeSet<String>, IndexSet<String>, HashSet<String> = {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
});
test_type!(BTreeMap<isize, String>, IndexMap<isize, String>, HashMap<isize, String> = {
"type": "object",
"properties": {
"default": {
"type": "integer"
}
},
"required": ["default"],
"additionalProperties": {
"type": "string"
}
});

View file

@ -7,7 +7,6 @@ use openapiv3::{
ReferenceOr::{Item, Reference}, ReferenceOr::{Item, Reference},
Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty
}; };
use std::{ use std::{
collections::{BTreeSet, HashMap, HashSet}, collections::{BTreeSet, HashMap, HashSet},
hash::BuildHasher, hash::BuildHasher,