diff --git a/README.md b/README.md index 812aad9b..e8c07678 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 `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/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..d8efd820 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::Uri) }), + #[cfg(feature = "time")] "PrimitiveDateTime" | "OffsetDateTime" => { tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::DateTime) }) @@ -402,35 +422,28 @@ pub enum Variant { Uuid, #[cfg(feature = "ulid")] Ulid, + #[cfg(feature = "url")] + Uri, 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", "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"))] - { - true - } - #[cfg(all(not(feature = "uuid"), feature = "ulid"))] - { - _format != &"Uuid" - } - #[cfg(all(feature = "uuid", not(feature = "ulid")))] - { - _format != &"Ulid" - } - #[cfg(all(not(feature = "uuid"), not(feature = "ulid")))] - { - _format != &"Uuid" && _format != &"Ulid" - } - }) + .filter(|_format| !excluded_format.contains(&_format)) .collect::>(); let lookahead = input.lookahead1(); @@ -452,6 +465,8 @@ impl Parse for Variant { "Uuid" => Ok(Self::Uuid), #[cfg(feature = "ulid")] "Ulid" => Ok(Self::Ulid), + #[cfg(feature = "url")] + "Uri" => Ok(Self::Uri), _ => Err(Error::new( format.span(), format!( @@ -507,6 +522,10 @@ impl ToTokens for Variant { Self::Ulid => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat( utoipa::openapi::KnownFormat::Ulid ))), + #[cfg(feature = "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 7e0f78cd..b22124dd 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#""uri""#, "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..d8c8bfd9 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 `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 1759ccd3..573a24c6 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")))] + Uri, } #[cfg(test)]