From 4064acb2e11cdde673edd083eaebcc8710d32739 Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Thu, 31 Aug 2023 10:49:01 +0200 Subject: [PATCH 1/3] Add url feature to support url type (String type with url format) --- README.md | 2 ++ scripts/doc.sh | 2 +- scripts/test.sh | 2 +- utoipa-gen/Cargo.toml | 2 ++ utoipa-gen/src/schema_type.rs | 44 ++++++++++++++++++++++---- utoipa-gen/tests/schema_derive_test.rs | 17 ++++++++++ utoipa/Cargo.toml | 3 +- utoipa/src/lib.rs | 2 ++ utoipa/src/openapi/schema.rs | 6 ++++ 9 files changed, 71 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 812aad9b..33c91af6 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also an awesome type of beer format `uuid` in OpenAPI spec. - **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with format `ulid` in OpenAPI spec. +- **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with + format `url` in OpenAPI spec. - **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`. - **openapi_extensions** Adds traits and functions that provide extra convenience functions. See the [`request_body` docs](https://docs.rs/utoipa/latest/utoipa/openapi/request_body) for an example. diff --git a/scripts/doc.sh b/scripts/doc.sh index 673e3612..349a3229 100755 --- a/scripts/doc.sh +++ b/scripts/doc.sh @@ -3,5 +3,5 @@ # Generate utoipa workspace docs cargo +nightly doc -Z unstable-options --workspace --no-deps \ - --features actix_extras,openapi_extensions,yaml,uuid,ulid,actix-web,axum,rocket \ + --features actix_extras,openapi_extensions,yaml,uuid,ulid,url,actix-web,axum,rocket \ --config 'build.rustdocflags = ["--cfg", "doc_cfg"]' diff --git a/scripts/test.sh b/scripts/test.sh index d71e17ef..3798394b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,7 +9,7 @@ echo "Testing crate: $crate..." if [[ "$crate" == "utoipa" ]]; then cargo test -p utoipa --features openapi_extensions,preserve_order,preserve_path_order,debug elif [[ "$crate" == "utoipa-gen" ]]; then - cargo test -p utoipa-gen --features utoipa/actix_extras,chrono,decimal,utoipa/uuid,uuid,utoipa/ulid,ulid,utoipa/time,time,utoipa/repr,utoipa/smallvec,smallvec,rc_schema,utoipa/rc_schema + cargo test -p utoipa-gen --features utoipa/actix_extras,chrono,decimal,utoipa/uuid,uuid,utoipa/ulid,ulid,utoipa/url,url,utoipa/time,time,utoipa/repr,utoipa/smallvec,smallvec,rc_schema,utoipa/rc_schema cargo test -p utoipa-gen --test path_derive_auto_into_responses --features auto_into_responses,utoipa/uuid,uuid cargo test -p utoipa-gen --test path_derive_actix --test path_parameter_derive_actix --features actix_extras,utoipa/uuid,uuid diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 6612e792..319320a6 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -20,6 +20,7 @@ proc-macro-error = "1.0" regex = { version = "1.7", optional = true } uuid = { version = "1", features = ["serde"], optional = true } ulid = { version = "1", optional = true, default-features = false } +url = { version = "2", optional = true } [dev-dependencies] utoipa = { path = "../utoipa", features = ["uuid"], default-features = false } @@ -47,6 +48,7 @@ rocket_extras = ["regex", "syn/extra-traits"] non_strict_integers = [] uuid = ["dep:uuid"] ulid = ["dep:ulid"] +url = ["dep:url"] axum_extras = ["regex", "syn/extra-traits"] time = [] smallvec = [] diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index 77ff65e6..1e0f47da 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -35,6 +35,7 @@ impl SchemaType<'_> { feature = "rocket_extras", feature = "uuid", feature = "ulid", + feature = "url", feature = "time", )))] { @@ -47,6 +48,7 @@ impl SchemaType<'_> { feature = "rocket_extras", feature = "uuid", feature = "ulid", + feature = "url", feature = "time", ))] { @@ -77,6 +79,11 @@ impl SchemaType<'_> { primitive = matches!(name, "Ulid"); } + #[cfg(feature = "url")] + if !primitive { + primitive = matches!(name, "Url"); + } + #[cfg(feature = "time")] if !primitive { primitive = matches!( @@ -208,6 +215,9 @@ impl ToTokens for SchemaType<'_> { #[cfg(feature = "ulid")] "Ulid" => tokens.extend(quote! { utoipa::openapi::SchemaType::String }), + #[cfg(feature = "url")] + "Url" => tokens.extend(quote! { utoipa::openapi::SchemaType::String }), + #[cfg(feature = "time")] "PrimitiveDateTime" | "OffsetDateTime" => { tokens.extend(quote! { utoipa::openapi::SchemaType::String }) @@ -275,6 +285,7 @@ impl Type<'_> { feature = "chrono", feature = "uuid", feature = "ulid", + feature = "url", feature = "time" )))] { @@ -285,6 +296,7 @@ impl Type<'_> { feature = "chrono", feature = "uuid", feature = "ulid", + feature = "url", feature = "time" ))] { @@ -305,6 +317,11 @@ impl Type<'_> { known_format = matches!(name, "Ulid"); } + #[cfg(feature = "url")] + if !known_format { + known_format = matches!(name, "Url"); + } + #[cfg(feature = "time")] if !known_format { known_format = matches!(name, "Date" | "PrimitiveDateTime" | "OffsetDateTime"); @@ -376,6 +393,9 @@ impl ToTokens for Type<'_> { #[cfg(feature = "ulid")] "Ulid" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Ulid) }), + #[cfg(feature = "url")] + "Url" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Url) }), + #[cfg(feature = "time")] "PrimitiveDateTime" | "OffsetDateTime" => { tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::DateTime) }) @@ -402,31 +422,37 @@ pub enum Variant { Uuid, #[cfg(feature = "ulid")] Ulid, + #[cfg(feature = "url")] + Url, Custom(String), } impl Parse for Variant { fn parse(input: syn::parse::ParseStream) -> syn::Result { - const FORMATS: [&str; 11] = [ + const FORMATS: [&str; 12] = [ "Int32", "Int64", "Float", "Double", "Byte", "Binary", "Date", "DateTime", "Password", - "Uuid", "Ulid", + "Uuid", "Ulid", "Url", ]; let known_formats = FORMATS .into_iter() .filter(|_format| { - #[cfg(all(feature = "uuid", feature = "ulid"))] + #[cfg(all(feature = "uuid", feature = "ulid", feature = "url"))] { true } - #[cfg(all(not(feature = "uuid"), feature = "ulid"))] + #[cfg(all(not(feature = "uuid"), feature = "ulid", not(feature = "url")))] { _format != &"Uuid" } - #[cfg(all(feature = "uuid", not(feature = "ulid")))] + #[cfg(all(feature = "uuid", not(feature = "ulid"), not(feature = "url")))] { _format != &"Ulid" } - #[cfg(all(not(feature = "uuid"), not(feature = "ulid")))] + #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), feature = "url"))] + { + _format != &"Url" + } + #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), not(feature = "url")))] { _format != &"Uuid" && _format != &"Ulid" } @@ -452,6 +478,8 @@ impl Parse for Variant { "Uuid" => Ok(Self::Uuid), #[cfg(feature = "ulid")] "Ulid" => Ok(Self::Ulid), + #[cfg(feature = "url")] + "Url" => Ok(Self::Url), _ => Err(Error::new( format.span(), format!( @@ -507,6 +535,10 @@ impl ToTokens for Variant { Self::Ulid => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat( utoipa::openapi::KnownFormat::Ulid ))), + #[cfg(feature = "url")] + Self::Url => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat( + utoipa::openapi::KnownFormat::Url + ))), Self::Custom(value) => tokens.extend(quote!(utoipa::openapi::SchemaFormat::Custom( String::from(#value) ))), diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 7e0f78cd..a88e257d 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -3165,6 +3165,23 @@ fn derive_struct_with_ulid_type() { } } +#[cfg(feature = "url")] +#[test] +fn derive_struct_with_url_type() { + use url::Url; + + let post = api_doc! { + struct Post { + id: Url, + } + }; + + assert_value! {post=> + "properties.id.type" = r#""string""#, "Post id type" + "properties.id.format" = r#""url""#, "Post id format" + } +} + #[test] fn derive_parse_serde_field_attributes() { struct S; diff --git a/utoipa/Cargo.toml b/utoipa/Cargo.toml index 21de26b9..503a07ef 100644 --- a/utoipa/Cargo.toml +++ b/utoipa/Cargo.toml @@ -31,6 +31,7 @@ non_strict_integers = ["utoipa-gen/non_strict_integers"] yaml = ["serde_yaml", "utoipa-gen/yaml"] uuid = ["utoipa-gen/uuid"] ulid = ["utoipa-gen/ulid"] +url = ["utoipa-gen/url"] time = ["utoipa-gen/time"] smallvec = ["utoipa-gen/smallvec"] indexmap = ["utoipa-gen/indexmap"] @@ -54,5 +55,5 @@ indexmap = { version = "2", features = ["serde"] } assert-json-diff = "2" [package.metadata.docs.rs] -features = ["actix_extras", "non_strict_integers", "openapi_extensions", "uuid", "ulid", "yaml"] +features = ["actix_extras", "non_strict_integers", "openapi_extensions", "uuid", "ulid", "url", "yaml"] rustdoc-args = ["--cfg", "doc_cfg"] diff --git a/utoipa/src/lib.rs b/utoipa/src/lib.rs index 4c999622..1fa8a854 100644 --- a/utoipa/src/lib.rs +++ b/utoipa/src/lib.rs @@ -70,6 +70,8 @@ //! format `uuid` in OpenAPI spec. //! * **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with //! format `ulid` in OpenAPI spec. +//! * **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with +//! format `url` in OpenAPI spec. //! * **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`. //! * **openapi_extensions** Adds convenience functions for documenting common scenarios, such as JSON request bodies and responses. //! See the [`request_body`](https://docs.rs/utoipa/latest/utoipa/openapi/request_body/index.html) and diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 1759ccd3..3b3a209b 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -1423,6 +1423,12 @@ pub enum KnownFormat { #[cfg(feature = "ulid")] #[cfg_attr(doc_cfg, doc(cfg(feature = "ulid")))] Ulid, + /// Used with [`String`] values to indicate value is in Url format. + /// + /// **url** feature need to be enabled. + #[cfg(feature = "url")] + #[cfg_attr(doc_cfg, doc(cfg(feature = "url")))] + Url, } #[cfg(test)] From 1f0f7be417a179a390ec154d5ba650ee49b8cfab Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Thu, 31 Aug 2023 11:04:13 +0200 Subject: [PATCH 2/3] Switch to uri format for url type --- README.md | 2 +- utoipa-gen/src/schema_type.rs | 16 ++++++++-------- utoipa-gen/tests/schema_derive_test.rs | 2 +- utoipa/src/lib.rs | 2 +- utoipa/src/openapi/schema.rs | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 33c91af6..e8c07678 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also an awesome type of beer - **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with format `ulid` in OpenAPI spec. - **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with - format `url` in OpenAPI spec. + format `uri` in OpenAPI spec. - **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`. - **openapi_extensions** Adds traits and functions that provide extra convenience functions. See the [`request_body` docs](https://docs.rs/utoipa/latest/utoipa/openapi/request_body) for an example. diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index 1e0f47da..f9183700 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -394,7 +394,7 @@ impl ToTokens for Type<'_> { "Ulid" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Ulid) }), #[cfg(feature = "url")] - "Url" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Url) }), + "Url" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Uri) }), #[cfg(feature = "time")] "PrimitiveDateTime" | "OffsetDateTime" => { @@ -423,7 +423,7 @@ pub enum Variant { #[cfg(feature = "ulid")] Ulid, #[cfg(feature = "url")] - Url, + Uri, Custom(String), } @@ -431,7 +431,7 @@ impl Parse for Variant { fn parse(input: syn::parse::ParseStream) -> syn::Result { const FORMATS: [&str; 12] = [ "Int32", "Int64", "Float", "Double", "Byte", "Binary", "Date", "DateTime", "Password", - "Uuid", "Ulid", "Url", + "Uuid", "Ulid", "Uri", ]; let known_formats = FORMATS .into_iter() @@ -450,11 +450,11 @@ impl Parse for Variant { } #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), feature = "url"))] { - _format != &"Url" + _format != &"Uri" } #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), not(feature = "url")))] { - _format != &"Uuid" && _format != &"Ulid" + _format != &"Uuid" && _format != &"Ulid" && _format != &"Uri" } }) .collect::>(); @@ -479,7 +479,7 @@ impl Parse for Variant { #[cfg(feature = "ulid")] "Ulid" => Ok(Self::Ulid), #[cfg(feature = "url")] - "Url" => Ok(Self::Url), + "Uri" => Ok(Self::Uri), _ => Err(Error::new( format.span(), format!( @@ -536,8 +536,8 @@ impl ToTokens for Variant { utoipa::openapi::KnownFormat::Ulid ))), #[cfg(feature = "url")] - Self::Url => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat( - utoipa::openapi::KnownFormat::Url + Self::Uri => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat( + utoipa::openapi::KnownFormat::Uri ))), Self::Custom(value) => tokens.extend(quote!(utoipa::openapi::SchemaFormat::Custom( String::from(#value) diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index a88e257d..b22124dd 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -3178,7 +3178,7 @@ fn derive_struct_with_url_type() { assert_value! {post=> "properties.id.type" = r#""string""#, "Post id type" - "properties.id.format" = r#""url""#, "Post id format" + "properties.id.format" = r#""uri""#, "Post id format" } } diff --git a/utoipa/src/lib.rs b/utoipa/src/lib.rs index 1fa8a854..d8c8bfd9 100644 --- a/utoipa/src/lib.rs +++ b/utoipa/src/lib.rs @@ -71,7 +71,7 @@ //! * **ulid** Add support for [ulid](https://github.com/dylanhart/ulid-rs). `Ulid` type will be presented as `String` with //! format `ulid` in OpenAPI spec. //! * **url** Add support for [url](https://github.com/servo/rust-url). `Url` type will be presented as `String` with -//! format `url` in OpenAPI spec. +//! format `uri` in OpenAPI spec. //! * **smallvec** Add support for [smallvec](https://crates.io/crates/smallvec). `SmallVec` will be treated as `Vec`. //! * **openapi_extensions** Adds convenience functions for documenting common scenarios, such as JSON request bodies and responses. //! See the [`request_body`](https://docs.rs/utoipa/latest/utoipa/openapi/request_body/index.html) and diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 3b3a209b..573a24c6 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -1428,7 +1428,7 @@ pub enum KnownFormat { /// **url** feature need to be enabled. #[cfg(feature = "url")] #[cfg_attr(doc_cfg, doc(cfg(feature = "url")))] - Url, + Uri, } #[cfg(test)] From 25ceeddb31d3e6a2f72c5d542ca7a4a73214edd3 Mon Sep 17 00:00:00 2001 From: Romain Lebran Date: Thu, 31 Aug 2023 11:35:03 +0200 Subject: [PATCH 3/3] Fix parse for variant --- utoipa-gen/src/schema_type.rs | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/utoipa-gen/src/schema_type.rs b/utoipa-gen/src/schema_type.rs index f9183700..d8efd820 100644 --- a/utoipa-gen/src/schema_type.rs +++ b/utoipa-gen/src/schema_type.rs @@ -433,30 +433,17 @@ impl Parse for Variant { "Int32", "Int64", "Float", "Double", "Byte", "Binary", "Date", "DateTime", "Password", "Uuid", "Ulid", "Uri", ]; + let excluded_format: &[&str] = &[ + #[cfg(not(feature = "uuid"))] + "Uuid", + #[cfg(not(feature = "ulid"))] + "Ulid", + #[cfg(not(feature = "url"))] + "Uri", + ]; let known_formats = FORMATS .into_iter() - .filter(|_format| { - #[cfg(all(feature = "uuid", feature = "ulid", feature = "url"))] - { - true - } - #[cfg(all(not(feature = "uuid"), feature = "ulid", not(feature = "url")))] - { - _format != &"Uuid" - } - #[cfg(all(feature = "uuid", not(feature = "ulid"), not(feature = "url")))] - { - _format != &"Ulid" - } - #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), feature = "url"))] - { - _format != &"Uri" - } - #[cfg(all(not(feature = "uuid"), not(feature = "ulid"), not(feature = "url")))] - { - _format != &"Uuid" && _format != &"Ulid" && _format != &"Uri" - } - }) + .filter(|_format| !excluded_format.contains(&_format)) .collect::>(); let lookahead = input.lookahead1();