From dc0cf3c18dc6e69b646815d32b262d189c0aca5c Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 16 Apr 2023 06:32:32 +0800 Subject: [PATCH] Allow additional integer types (#575) This PR adds a new `non_strict_integers` feature flag to allow use of non-standard integer formats added in #571 at OpenAPI spec. --- README.md | 1 + utoipa-gen/Cargo.toml | 2 + utoipa-gen/src/schema_type.rs | 21 ++++- utoipa-gen/tests/path_derive.rs | 12 +-- .../tests/path_parameter_derive_test.rs | 2 +- utoipa-gen/tests/path_response_derive_test.rs | 4 +- utoipa-gen/tests/request_body_derive_test.rs | 6 +- utoipa-gen/tests/schema_derive_test.rs | 62 ++++++++++++++- utoipa/Cargo.toml | 4 +- utoipa/src/lib.rs | 78 ++++++++++++++++--- utoipa/src/openapi/schema.rs | 12 +++ 11 files changed, 173 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ac99b5a9..f7eafc88 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also an awesome type of beer When disabled, the properties are listed in alphabetical order. - **indexmap** Add support for [indexmap](https://crates.io/crates/indexmap). When enabled `IndexMap` will be rendered as a map similar to `BTreeMap` and `HashMap`. +- **non_strict_integers** Add support for non-standard integer formats `int8`, `int16`, `uint8`, `uint16`, `uint32`, and `uint64`. Utoipa implicitly has partial support for `serde` attributes. See [docs](https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#partial-serde-attributes-support) for more details. diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 15deef12..b51f94e0 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -37,12 +37,14 @@ time = { version = "0.3", features = ["serde-human-readable"] } serde_with = "2.3" [features] +# See README.md for list and explanations of features debug = ["syn/extra-traits"] actix_extras = ["regex", "lazy_static", "syn/extra-traits"] chrono = [] yaml = [] decimal = [] rocket_extras = ["regex", "lazy_static", "syn/extra-traits"] +non_strict_integers = [] uuid = ["dep:uuid"] axum_extras = ["syn/extra-traits"] time = [] diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index bf37261e..fef9f385 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -300,10 +300,27 @@ impl ToTokens for Type<'_> { let name = &*last_segment.ident.to_string(); match name { - "i8" | "i16" | "i32" | "u8" | "u16" | "u32" => { + #[cfg(feature="non_strict_integers")] + "i8" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int8) }), + #[cfg(feature="non_strict_integers")] + "u8" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::UInt8) }), + #[cfg(feature="non_strict_integers")] + "i16" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int16) }), + #[cfg(feature="non_strict_integers")] + "u16" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::UInt16) }), + #[cfg(feature="non_strict_integers")] + #[cfg(feature="non_strict_integers")] + "u32" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::UInt32) }), + #[cfg(feature="non_strict_integers")] + "u64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::UInt64) }), + #[cfg(not(feature="non_strict_integers"))] + "i8" | "i16" | "u8" | "u16" | "u32" => { tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int32) }) } - "i64" | "u64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64) }), + #[cfg(not(feature="non_strict_integers"))] + "u64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64) }), + "i32" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int32) }), + "i64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64) }), "f32" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Float) }), "f64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Double) }), #[cfg(feature = "chrono")] diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index 212e589c..1e7536b2 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -205,7 +205,7 @@ fn derive_path_with_extra_attributes_without_nested_module() { status = 200, description = "success response") ), params( - ("id" = u64, deprecated = false, description = "Foo database id"), + ("id" = i64, deprecated = false, description = "Foo database id"), ("since" = Option, Query, deprecated = false, description = "Datetime since foo is updated") ) )] @@ -233,7 +233,6 @@ fn derive_path_with_extra_attributes_without_nested_module() { "parameters.[0].in" = r#""path""#, "Parameter 0 in" "parameters.[0].name" = r#""id""#, "Parameter 0 name" "parameters.[0].required" = r#"true"#, "Parameter 0 required" - "parameters.[0].schema.minimum" = r#"0.0"#, "Parameter 0 minimum" "parameters.[0].schema.format" = r#""int64""#, "Parameter 0 schema format" "parameters.[0].schema.type" = r#""integer""#, "Parameter 0 schema type" @@ -302,7 +301,7 @@ fn derive_path_with_parameter_schema() { (status = 200, description = "success response") ), params( - ("id" = u64, description = "Foo database id"), + ("id" = i64, description = "Foo database id"), ("since" = Option, Query, description = "Datetime since foo is updated") ) )] @@ -330,7 +329,6 @@ fn derive_path_with_parameter_schema() { "schema": { "format": "int64", "type": "integer", - "minimum": 0.0, } }, { @@ -373,7 +371,7 @@ fn derive_path_with_parameter_inline_schema() { (status = 200, description = "success response") ), params( - ("id" = u64, description = "Foo database id"), + ("id" = i64, description = "Foo database id"), ("since" = inline(Option), Query, description = "Datetime since foo is updated") ) )] @@ -401,7 +399,6 @@ fn derive_path_with_parameter_inline_schema() { "schema": { "format": "int64", "type": "integer", - "minimum": 0.0, } }, { @@ -954,7 +951,7 @@ fn derive_path_params_intoparams() { /// Foo database id. #[param(example = 1)] #[allow(unused)] - id: u64, + id: i64, /// Datetime since foo is updated. #[param(example = "2020-04-12T10:23:00Z")] #[allow(unused)] @@ -1012,7 +1009,6 @@ fn derive_path_params_intoparams() { "schema": { "format": "int64", "type": "integer", - "minimum": 0.0, }, "style": "form" }, diff --git a/utoipa-gen/tests/path_parameter_derive_test.rs b/utoipa-gen/tests/path_parameter_derive_test.rs index e694fc6d..67dab075 100644 --- a/utoipa-gen/tests/path_parameter_derive_test.rs +++ b/utoipa-gen/tests/path_parameter_derive_test.rs @@ -151,7 +151,7 @@ mod mod_derive_parameters_all_types { params( ("id" = i32, Path, description = "Foo id"), ("since" = String, Query, deprecated, description = "Datetime since"), - ("numbers" = Option<[u64]>, Query, description = "Foo numbers list"), + ("numbers" = Option<[i64]>, Query, description = "Foo numbers list"), ("token" = String, Header, deprecated, description = "Token of foo"), ("cookieval" = String, Cookie, deprecated, description = "Foo cookie"), ) diff --git a/utoipa-gen/tests/path_response_derive_test.rs b/utoipa-gen/tests/path_response_derive_test.rs index 3e6492ba..fa8b3178 100644 --- a/utoipa-gen/tests/path_response_derive_test.rs +++ b/utoipa-gen/tests/path_response_derive_test.rs @@ -229,7 +229,7 @@ object_body_with_multiple_headers => body: Foo, headers: ( "responses.200.headers.another-header.schema.type" = r###""string""###, "another-header header type" "responses.200.headers.another-header.description" = r###"null"###, "another-header header description" object_body_with_header_with_type => body: Foo, headers: ( - ("random-digits" = [u64]), + ("random-digits" = [i64]), ), assert: "responses.200.content.application~1json.schema.$ref" = r###""#/components/schemas/Foo""###, "Response content type" "responses.200.headers.random-digits.schema.type" = r###""array""###, "random-digits header type" @@ -237,7 +237,7 @@ object_body_with_header_with_type => body: Foo, headers: ( "responses.200.headers.random-digits.schema.items.type" = r###""integer""###, "random-digits header items type" "responses.200.headers.random-digits.schema.items.format" = r###""int64""###, "random-digits header items format" response_no_body_with_complex_header_with_description => headers: ( - ("random-digits" = [u64], description = "Random digits response header"), + ("random-digits" = [i64], description = "Random digits response header"), ), assert: "responses.200.content" = r###"null"###, "Response content type" "responses.200.headers.random-digits.description" = r###""Random digits response header""###, "random-digits header description" diff --git a/utoipa-gen/tests/request_body_derive_test.rs b/utoipa-gen/tests/request_body_derive_test.rs index 286603a6..eb9a02ac 100644 --- a/utoipa-gen/tests/request_body_derive_test.rs +++ b/utoipa-gen/tests/request_body_derive_test.rs @@ -130,7 +130,7 @@ fn derive_request_body_primitive_simple_success() { test_fn! { module: derive_request_body_primitive_simple_array, - body: = [u64] + body: = [i64] } #[test] @@ -154,7 +154,6 @@ fn derive_request_body_primitive_array_success() { "items": { "type": "integer", "format": "int64", - "minimum": 0.0, } } } @@ -389,7 +388,7 @@ fn derive_request_body_complex_required_explicit_false_success() { test_fn! { module: derive_request_body_complex_primitive_array, - body: (content = [u32], description = "Create new foo references") + body: (content = [i32], description = "Create new foo references") } #[test] @@ -412,7 +411,6 @@ fn derive_request_body_complex_primitive_array_success() { "items": { "type": "integer", "format": "int32", - "minimum": 0.0, } } } diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 94ce81d7..3b4b6b00 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -300,7 +300,7 @@ fn derive_struct_with_optional_properties() { let owner = api_doc! { struct Owner { #[schema(default = 1)] - id: u64, + id: i64, enabled: Option, books: Option>, metadata: Option>, @@ -316,7 +316,6 @@ fn derive_struct_with_optional_properties() { "type": "integer", "format": "int64", "default": 1, - "minimum": 0.0, }, "enabled": { "type": "boolean", @@ -647,8 +646,8 @@ fn derive_unnamed_struct_deprecated_success() { #[test] fn derive_unnamed_struct_example_json_array_success() { let pet_age = api_doc! { - #[schema(example = "0", default = u64::default)] - struct PetAge(u64, u64); + #[schema(example = "0", default = i64::default)] + struct PetAge(i64, i64); }; assert_value! {pet_age=> @@ -3811,6 +3810,61 @@ fn derive_struct_with_validation_fields() { } }; + #[cfg(feature = "non_strict_integers")] + assert_json_eq!( + value, + json!({ + "properties": { + "id": { + "format": "int32", + "type": "integer", + "maximum": 10.0, + "minimum": 5.0, + "multipleOf": 2.5, + }, + "value": { + "type": "string", + "maxLength": 10, + "minLength": 5, + "pattern": "[a-z]*" + }, + "items": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + }, + "maxItems": 5, + "minItems": 1, + }, + "unsigned": { + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "unsigned_value": { + "type": "integer", + "format": "uint32", + "minimum": 2.0, + } + "unsigned_value": { + "type": "integer", + "format": "int32", + "minimum": 2.0, + } + }, + "type": "object", + "required": [ + "id", + "value", + "items", + "unsigned", + "unsigned_value" + ] + }) + ); + + #[cfg(not(feature = "non_strict_integers"))] assert_json_eq!( value, json!({ diff --git a/utoipa/Cargo.toml b/utoipa/Cargo.toml index 438316fc..86742fd1 100644 --- a/utoipa/Cargo.toml +++ b/utoipa/Cargo.toml @@ -19,6 +19,7 @@ categories = ["web-programming"] authors = ["Juha Kukkonen "] [features] +# See README.md for list and explanations of features default = [] debug = ["utoipa-gen/debug"] actix_extras = ["utoipa-gen/actix_extras"] @@ -26,6 +27,7 @@ rocket_extras = ["utoipa-gen/rocket_extras"] axum_extras = ["utoipa-gen/axum_extras"] chrono = ["utoipa-gen/chrono"] decimal = ["utoipa-gen/decimal"] +non_strict_integers = ["utoipa-gen/non_strict_integers"] yaml = ["serde_yaml", "utoipa-gen/yaml"] uuid = ["utoipa-gen/uuid"] time = ["utoipa-gen/time"] @@ -46,5 +48,5 @@ indexmap = { version = "1", features = ["serde"] } assert-json-diff = "2" [package.metadata.docs.rs] -features = ["actix_extras", "openapi_extensions", "yaml", "uuid"] +features = ["actix_extras", "non_strict_integers", "openapi_extensions", "uuid", "yaml"] rustdoc-args = ["--cfg", "doc_cfg"] diff --git a/utoipa/src/lib.rs b/utoipa/src/lib.rs index 68c4319f..65cb1e3d 100644 --- a/utoipa/src/lib.rs +++ b/utoipa/src/lib.rs @@ -79,6 +79,7 @@ //! When disabled, the properties are listed in alphabetical order. //! * **indexmap** Add support for [indexmap](https://crates.io/crates/indexmap). When enabled `IndexMap` will be rendered as a map similar to //! `BTreeMap` and `HashMap`. +//! * **non_strict_integers** Add support for non-standard integer formats `int8`, `int16`, `uint8`, `uint16`, `uint32`, and `uint64`. //! //! Utoipa implicitly has partial support for `serde` attributes. See [`ToSchema` derive][serde] for more details. //! @@ -464,7 +465,7 @@ mod utoipa { /// # use utoipa::openapi::schema::{SchemaType, KnownFormat, SchemaFormat, ObjectBuilder, Schema}; /// # use utoipa::openapi::RefOr; /// # -/// let number: RefOr = u64::schema().into(); +/// let number: RefOr = i64::schema().into(); /// /// // would be equal to manual implementation /// let number2 = RefOr::T( @@ -472,7 +473,6 @@ mod utoipa { /// ObjectBuilder::new() /// .schema_type(SchemaType::Integer) /// .format(Some(SchemaFormat::KnownFormat(KnownFormat::Int64))) -/// .minimum(Some(0.0)) /// .build() /// ) /// ); @@ -913,8 +913,9 @@ mod tests { use super::*; + #[cfg(not(feature = "non_strict_integers"))] #[test] - fn test_partial_schema() { + fn test_partial_schema_strict_integers() { for (name, schema, value) in [ ( "i8", @@ -958,16 +959,75 @@ mod tests { u64::schema(), json!({"type": "integer", "format": "int64", "minimum": 0.0}), ), + ] { + println!( + "{name}: {json}", + json = serde_json::to_string(&schema).unwrap() + ); + let schema = serde_json::to_value(schema).unwrap(); + assert_json_eq!(schema, value); + } + } + + #[cfg(feature = "non_strict_integers")] + #[test] + fn test_partial_schema_non_strict_integers() { + for (name, schema, value) in [ + ( + "i8", + i8::schema(), + json!({"type": "integer", "format": "int8"}), + ), + ( + "i16", + i16::schema(), + json!({"type": "integer", "format": "int16"}), + ), + ( + "i32", + i32::schema(), + json!({"type": "integer", "format": "int32"}), + ), + ( + "i64", + i64::schema(), + json!({"type": "integer", "format": "int64"}), + ), + ("i128", i128::schema(), json!({"type": "integer"})), + ("isize", isize::schema(), json!({"type": "integer"})), ( - "u128", - u128::schema(), - json!({"type": "integer", "minimum": 0.0}), + "u8", + u8::schema(), + json!({"type": "integer", "format": "uint8", "minimum": 0.0}), ), ( - "usize", - usize::schema(), - json!({"type": "integer", "minimum": 0.0 }), + "u16", + u16::schema(), + json!({"type": "integer", "format": "uint16", "minimum": 0.0}), ), + ( + "u32", + u32::schema(), + json!({"type": "integer", "format": "uint32", "minimum": 0.0}), + ), + ( + "u64", + u64::schema(), + json!({"type": "integer", "format": "uint64", "minimum": 0.0}), + ), + ] { + println!( + "{name}: {json}", + json = serde_json::to_string(&schema).unwrap() + ); + let schema = serde_json::to_value(schema).unwrap(); + assert_json_eq!(schema, value); + } + } + + #[test] + fn test_partial_schema() { + for (name, schema, value) in [ ("bool", bool::schema(), json!({"type": "boolean"})), ("str", str::schema(), json!({"type": "string"})), ("String", String::schema(), json!({"type": "string"})), diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 340fc6ff..b4e464b9 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -1192,20 +1192,32 @@ pub enum SchemaFormat { #[serde(rename_all = "lowercase")] pub enum KnownFormat { /// 8 bit integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] Int8, /// 16 bit integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] Int16, /// 32 bit integer. Int32, /// 64 bit integer. Int64, /// 8 bit unsigned integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] UInt8, /// 16 bit unsigned integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] UInt16, /// 32 bit unsigned integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] UInt32, /// 64 bit unsigned integer. + #[cfg(feature = "non_strict_integers")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "non_strict_integers")))] UInt64, /// floating point number. Float,