From eecd1924580d958233ea8a3dd7ba32b6000572ab Mon Sep 17 00:00:00 2001 From: Dominic Date: Tue, 9 Mar 2021 17:07:16 +0100 Subject: [PATCH] redo and test openapi type implementations --- openapi_type/Cargo.toml | 4 + openapi_type/src/impls.rs | 483 +++++++++++++------------------- openapi_type/tests/std_types.rs | 216 ++++++++++++++ src/openapi/types.rs | 1 - 4 files changed, 413 insertions(+), 291 deletions(-) create mode 100644 openapi_type/tests/std_types.rs diff --git a/openapi_type/Cargo.toml b/openapi_type/Cargo.toml index 0513c0b..782f798 100644 --- a/openapi_type/Cargo.toml +++ b/openapi_type/Cargo.toml @@ -17,6 +17,10 @@ openapi_type_derive = "0.1.0-dev" openapiv3 = "=0.3.2" serde_json = "1.0" +# optional dependencies / features +chrono = { version = "0.4.19", optional = true } +uuid = { version = "0.8.2" , optional = true } + [dev-dependencies] paste = "1.0" serde = "1.0" diff --git a/openapi_type/src/impls.rs b/openapi_type/src/impls.rs index f363818..d9396fd 100644 --- a/openapi_type/src/impls.rs +++ b/openapi_type/src/impls.rs @@ -1,321 +1,224 @@ use crate::{OpenapiSchema, OpenapiType}; -use indexmap::IndexMap; +#[cfg(feature = "chrono")] +use chrono::{offset::TimeZone, Date, DateTime, NaiveDate, NaiveDateTime}; +use indexmap::{IndexMap, IndexSet}; use openapiv3::{ - AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, StringType, - Type, VariantOrUnknownOrEmpty + AdditionalProperties, ArrayType, IntegerType, NumberFormat, NumberType, ObjectType, ReferenceOr, SchemaKind, + StringFormat, StringType, Type, VariantOrUnknownOrEmpty }; +use serde_json::Value; use std::{ - collections::{BTreeSet, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, hash::BuildHasher, num::{NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize} }; +#[cfg(feature = "uuid")] +use uuid::Uuid; -impl OpenapiType for () { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Object(ObjectType { - additional_properties: Some(AdditionalProperties::Any(false)), - ..Default::default() - }))) +macro_rules! impl_openapi_type { + ($($ty:ident $(<$($generic:ident : $bound:path),+>)*),* => $schema:expr) => { + $( + impl $(<$($generic : $bound),+>)* OpenapiType for $ty $(<$($generic),+>)* { + 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, bits: Option) -> 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 { - fn schema() -> OpenapiSchema { - OpenapiSchema::new(SchemaKind::Type(Type::Boolean {})) - } +impl_openapi_type!(isize => int_schema(None, None)); +impl_openapi_type!(i8 => int_schema(None, Some(8))); +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 { - ($($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - OpenapiSchema::new(SchemaKind::Type(Type::Integer(IntegerType::default()))) - } - } - )*}; +impl_openapi_type!(f32 => float_schema(NumberFormat::Float)); +impl_openapi_type!(f64 => float_schema(NumberFormat::Double)); - (unsigned $($int_ty:ty),*) => {$( - impl OpenapiType for $int_ty - { - fn schema() -> OpenapiSchema - { - 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() - }))) - } - } - )*}; +#[inline] +fn str_schema(format: VariantOrUnknownOrEmpty) -> OpenapiSchema { + OpenapiSchema::new(SchemaKind::Type(Type::String(StringType { + format, + ..Default::default() + }))) } -int_types!(isize); -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); +impl_openapi_type!(String => str_schema(VariantOrUnknownOrEmpty::Empty)); #[cfg(feature = "chrono")] -str_types!(format = Date, Date, Date, Date, NaiveDate); +impl_openapi_type!(Date, NaiveDate => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::Date)) +}); + #[cfg(feature = "chrono")] -str_types!( - format = DateTime, - DateTime, - DateTime, - DateTime, - NaiveDateTime -); +impl_openapi_type!(DateTime, NaiveDateTime => { + str_schema(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime)) +}); #[cfg(feature = "uuid")] -str_types!(format_str = "uuid", Uuid); +impl_openapi_type!(Uuid => { + str_schema(VariantOrUnknownOrEmpty::Unknown("uuid".to_owned())) +}); -impl OpenapiType for Option { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); - let schema = match schema.name.clone() { - Some(name) => { - let reference = ReferenceOr::Reference { - reference: format!("#/components/schemas/{}", name) - }; - dependencies.insert(name, schema); - SchemaKind::AllOf { all_of: vec![reference] } - }, - None => schema.schema - }; +impl_openapi_type!(Option => { + let schema = T::schema(); + let mut dependencies = schema.dependencies.clone(); + let schema = match schema.name.clone() { + Some(name) => { + let reference = ReferenceOr::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 - } + OpenapiSchema { + nullable: true, + name: None, + schema, + dependencies + } +}); + +#[inline] +fn array_schema(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 OpenapiType for Vec { - fn schema() -> OpenapiSchema { - let schema = T::schema(); - let mut dependencies = schema.dependencies.clone(); +impl_openapi_type!(Vec => array_schema::(false)); +impl_openapi_type!(BTreeSet, IndexSet, HashSet => { + array_schema::(true) +}); - 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())) - }; +#[inline] +fn map_schema() -> OpenapiSchema { + let key_schema = K::schema(); + let mut dependencies = key_schema.dependencies.clone(); - OpenapiSchema { - nullable: false, - name: None, - schema: SchemaKind::Type(Type::Array(ArrayType { - items, - min_items: None, - max_items: None, - unique_items: false - })), - dependencies - } + 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 BTreeSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashSet { - fn schema() -> OpenapiSchema { - as OpenapiType>::schema() - } -} - -impl OpenapiType for HashMap { - 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() - } - } -} +impl_openapi_type!( + BTreeMap, + IndexMap, + HashMap + => map_schema::() +); diff --git a/openapi_type/tests/std_types.rs b/openapi_type/tests/std_types.rs new file mode 100644 index 0000000..e10fb89 --- /dev/null +++ b/openapi_type/tests/std_types.rs @@ -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, Date, Date, NaiveDate = { + "type": "string", + "format": "date" +}); + +#[cfg(feature = "chrono")] +test_type!(DateTime, DateTime, DateTime, NaiveDateTime = { + "type": "string", + "format": "date-time" +}); + +// ### some std types + +test_type!(Option = { + "type": "string", + "nullable": true +}); + +test_type!(Vec = { + "type": "array", + "items": { + "type": "string" + } +}); + +test_type!(BTreeSet, IndexSet, HashSet = { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true +}); + +test_type!(BTreeMap, IndexMap, HashMap = { + "type": "object", + "properties": { + "default": { + "type": "integer" + } + }, + "required": ["default"], + "additionalProperties": { + "type": "string" + } +}); diff --git a/src/openapi/types.rs b/src/openapi/types.rs index 18f5be6..66bf059 100644 --- a/src/openapi/types.rs +++ b/src/openapi/types.rs @@ -7,7 +7,6 @@ use openapiv3::{ ReferenceOr::{Item, Reference}, Schema, SchemaData, SchemaKind, StringType, Type, VariantOrUnknownOrEmpty }; - use std::{ collections::{BTreeSet, HashMap, HashSet}, hash::BuildHasher,