Skip to content

Commit

Permalink
Allow custom SchemaFormats (#315)
Browse files Browse the repository at this point in the history
As per the OpenAPI spec, allows custom values to be used as a format. 
Update docs and add tests.
  • Loading branch information
jacob-pro committed Oct 10, 2022
1 parent 00fb59a commit 3180d18
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 64 deletions.
16 changes: 9 additions & 7 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ use ext::ArgumentResolver;
/// # Unnamed Field Struct Optional Configuration Options for `#[schema(...)]`
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `format = ...` Any variant of a [`SchemaFormat`][format] to use for the property. By default the format is derived from
/// the type of the property according OpenApi spec.
/// * `format = ...` May either be variant of the [`KnownFormat`][known_format] enum, or otherwise
/// an open value as a string. By default the format is derived from the type of the property
/// according OpenApi spec.
/// * `value_type = ...` Can be used to override default type derived from type of the field used in OpenAPI spec.
/// This is useful in cases where the default type does not correspond to the actual type e.g. when
/// any third-party types are used which are not [`ToSchema`][to_schema]s nor [`primitive` types][primitive].
Expand All @@ -86,8 +87,9 @@ use ext::ArgumentResolver;
/// # Named Fields Optional Configuration Options for `#[schema(...)]`
/// * `example = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `default = ...` Can be literal value, method reference or _`json!(...)`_. [^json2]
/// * `format = ...` Any variant of a [`SchemaFormat`][format] to use for the property. By default the format is derived from
/// the type of the property according OpenApi spec.
/// * `format = ...` May either be variant of the [`KnownFormat`][known_format] enum, or otherwise
/// an open value as a string. By default the format is derived from the type of the property
/// according OpenApi spec.
/// * `write_only` Defines property is only used in **write** operations *POST,PUT,PATCH* but not in *GET*
/// * `read_only` Defines property is only used in **read** operations *GET* but not in *POST,PUT,PATCH*
/// * `xml(...)` Can be used to define [`Xml`][xml] object properties applicable to named fields.
Expand Down Expand Up @@ -368,7 +370,7 @@ use ext::ArgumentResolver;
/// ```
///
/// Enforce type being used in OpenAPI spec to [`String`] with `value_type` and set format to octet stream
/// with [`SchemaFormat::Binary`][binary].
/// with [`SchemaFormat::KnownFormat(KnownFormat::Binary)`][binary].
/// ```rust
/// # use utoipa::ToSchema;
/// #[derive(ToSchema)]
Expand Down Expand Up @@ -420,8 +422,8 @@ use ext::ArgumentResolver;
/// More examples for _`value_type`_ in [`IntoParams` derive docs][into_params].
///
/// [to_schema]: trait.ToSchema.html
/// [format]: openapi/schema/enum.SchemaFormat.html
/// [binary]: openapi/schema/enum.SchemaFormat.html#variant.Binary
/// [known_format]: openapi/schema/enum.KnownFormat.html
/// [binary]: openapi/schema/enum.KnownFormat.html#variant.Binary
/// [xml]: openapi/xml/struct.Xml.html
/// [into_params]: derive.IntoParams.html
/// [primitive]: https://doc.rust-lang.org/std/primitive/index.html
Expand Down
118 changes: 73 additions & 45 deletions utoipa-gen/src/schema_type.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro2::TokenStream;
use proc_macro_error::abort_call_site;
use quote::{quote, ToTokens};
use syn::{parse::Parse, Error, Ident, Path};
use syn::{parse::Parse, Error, Ident, LitStr, Path};

/// Tokenizes OpenAPI data type correctly according to the Rust type
pub struct SchemaType<'a>(pub &'a syn::Path);
Expand Down Expand Up @@ -239,19 +239,19 @@ impl ToTokens for Type<'_> {

match name {
"i8" | "i16" | "i32" | "u8" | "u16" | "u32" => {
tokens.extend(quote! { utoipa::openapi::SchemaFormat::Int32 })
tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int32) })
}
"i64" | "u64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::Int64 }),
"f32" | "f64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::Float }),
"i64" | "u64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64) }),
"f32" | "f64" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Float) }),
#[cfg(feature = "chrono")]
"DateTime" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::DateTime }),
"DateTime" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::DateTime) }),
#[cfg(any(feature = "chrono", feature = "Time"))]
"Date" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::Date }),
"Date" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Date) }),
#[cfg(feature = "uuid")]
"Uuid" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::Uuid }),
"Uuid" => tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Uuid) }),
#[cfg(feature = "time")]
"PrimitiveDateTime" | "OffsetDateTime" => {
tokens.extend(quote! { utoipa::openapi::SchemaFormat::DateTime })
tokens.extend(quote! { utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::DateTime) })
}
_ => (),
}
Expand All @@ -272,6 +272,7 @@ pub enum Variant {
Password,
#[cfg(feature = "uuid")]
Uuid,
Custom(String),
}

impl Parse for Variant {
Expand All @@ -280,7 +281,7 @@ impl Parse for Variant {
"Int32", "Int64", "Float", "Double", "Byte", "Binary", "Date", "DateTime", "Password",
"Uuid",
];
let allowed_formats = FORMATS
let known_formats = FORMATS
.into_iter()
.filter(|_format| {
#[cfg(feature = "uuid")]
Expand All @@ -293,51 +294,78 @@ impl Parse for Variant {
}
})
.collect::<Vec<_>>();
let expected_formats = format!(
"unexpected format, expected one of: {}",
allowed_formats.join(", ")
);
let format = input.parse::<Ident>()?;
let name = &*format.to_string();

match name {
"Int32" => Ok(Self::Int32),
"Int64" => Ok(Self::Int64),
"Float" => Ok(Self::Float),
"Double" => Ok(Self::Double),
"Byte" => Ok(Self::Byte),
"Binary" => Ok(Self::Binary),
"Date" => Ok(Self::Date),
"DateTime" => Ok(Self::DateTime),
"Password" => Ok(Self::Password),
#[cfg(feature = "uuid")]
"Uuid" => Ok(Self::Uuid),
_ => Err(Error::new(
format.span(),
format!("unexpected format: {name}, expected one of: {expected_formats}"),
)),
let lookahead = input.lookahead1();
if lookahead.peek(Ident) {
let format = input.parse::<Ident>()?;
let name = &*format.to_string();

match name {
"Int32" => Ok(Self::Int32),
"Int64" => Ok(Self::Int64),
"Float" => Ok(Self::Float),
"Double" => Ok(Self::Double),
"Byte" => Ok(Self::Byte),
"Binary" => Ok(Self::Binary),
"Date" => Ok(Self::Date),
"DateTime" => Ok(Self::DateTime),
"Password" => Ok(Self::Password),
#[cfg(feature = "uuid")]
"Uuid" => Ok(Self::Uuid),
_ => Err(Error::new(
format.span(),
format!(
"unexpected format: {name}, expected one of: {}",
known_formats.join(", ")
),
)),
}
} else if lookahead.peek(LitStr) {
let value = input.parse::<LitStr>()?.value();
Ok(Self::Custom(value))
} else {
Err(lookahead.error())
}
}
}

impl ToTokens for Variant {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Int32 => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Int32)),
Self::Int64 => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Int64)),
Self::Float => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Float)),
Self::Double => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Double)),
Self::Byte => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Byte)),
Self::Binary => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Binary)),
Self::Date => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Date)),
Self::DateTime => {
tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::DateTime))
}
Self::Password => {
tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Password))
}
Self::Int32 => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int32
))),
Self::Int64 => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Int64
))),
Self::Float => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Float
))),
Self::Double => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Double
))),
Self::Byte => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Byte
))),
Self::Binary => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Binary
))),
Self::Date => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Date
))),
Self::DateTime => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::DateTime
))),
Self::Password => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Password
))),
#[cfg(feature = "uuid")]
Self::Uuid => tokens.extend(quote!(utoipa::openapi::schema::SchemaFormat::Uuid)),
Self::Uuid => tokens.extend(quote!(utoipa::openapi::SchemaFormat::KnownFormat(
utoipa::openapi::KnownFormat::Uuid
))),
Self::Custom(value) => tokens.extend(quote!(utoipa::openapi::SchemaFormat::Custom(
String::from(#value)
))),
};
}
}
4 changes: 2 additions & 2 deletions utoipa-gen/tests/path_derive_actix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde_json::Value;
use utoipa::{
openapi::{
path::{Parameter, ParameterBuilder, ParameterIn},
Array, ObjectBuilder, SchemaFormat,
Array, KnownFormat, ObjectBuilder, SchemaFormat,
},
IntoParams, OpenApi, ToSchema,
};
Expand Down Expand Up @@ -360,7 +360,7 @@ fn path_with_struct_variables_with_into_params() {
.schema(Some(
ObjectBuilder::new()
.schema_type(utoipa::openapi::SchemaType::Integer)
.format(Some(SchemaFormat::Int64)),
.format(Some(SchemaFormat::KnownFormat(KnownFormat::Int64))),
))
.parameter_in(ParameterIn::Path)
.build(),
Expand Down
15 changes: 15 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,21 @@ fn derive_struct_component_field_type_override_with_format() {
}
}

#[test]
fn derive_struct_component_field_type_override_with_custom_format() {
let post = api_doc! {
struct Post {
#[schema(value_type = String, format = "uri")]
value: String,
}
};

assert_value! {post=>
"properties.value.type" = r#""string""#, "Post value type"
"properties.value.format" = r#""uri""#, "Post value format"
}
}

#[test]
fn derive_struct_component_field_type_override_with_format_with_vec() {
let post = api_doc! {
Expand Down
8 changes: 4 additions & 4 deletions utoipa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ pub trait OpenApi {
/// "id",
/// utoipa::openapi::ObjectBuilder::new()
/// .schema_type(utoipa::openapi::SchemaType::Integer)
/// .format(Some(utoipa::openapi::SchemaFormat::Int64)),
/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64))),
/// )
/// .required("id")
/// .property(
Expand All @@ -318,7 +318,7 @@ pub trait OpenApi {
/// "age",
/// utoipa::openapi::ObjectBuilder::new()
/// .schema_type(utoipa::openapi::SchemaType::Integer)
/// .format(Some(utoipa::openapi::SchemaFormat::Int32)),
/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int32))),
/// )
/// .example(Some(serde_json::json!({
/// "name": "bob the cat", "id": 1
Expand Down Expand Up @@ -407,7 +407,7 @@ pub trait ToSchema {
/// .schema(
/// Some(utoipa::openapi::ObjectBuilder::new()
/// .schema_type(utoipa::openapi::SchemaType::Integer)
/// .format(Some(utoipa::openapi::SchemaFormat::Int64))),
/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64)))),
/// ),
/// )
/// .tag("pet_api"),
Expand Down Expand Up @@ -532,7 +532,7 @@ pub trait Modify {
/// .schema(Some(
/// utoipa::openapi::ObjectBuilder::new()
/// .schema_type(utoipa::openapi::SchemaType::Integer)
/// .format(Some(utoipa::openapi::SchemaFormat::Int64)),
/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(utoipa::openapi::KnownFormat::Int64))),
/// ))
/// .build(),
/// utoipa::openapi::path::ParameterBuilder::new()
Expand Down
3 changes: 2 additions & 1 deletion utoipa/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ pub use self::{
response::{Response, ResponseBuilder, Responses, ResponsesBuilder},
schema::{
AllOf, AllOfBuilder, Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator,
Object, ObjectBuilder, OneOf, OneOfBuilder, Ref, Schema, SchemaFormat, SchemaType, ToArray,
KnownFormat, Object, ObjectBuilder, OneOf, OneOfBuilder, Ref, Schema, SchemaFormat,
SchemaType, ToArray,
},
security::SecurityRequirement,
server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder},
Expand Down
20 changes: 15 additions & 5 deletions utoipa/src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1117,10 +1117,20 @@ impl Default for SchemaType {

/// Additional format for [`SchemaType`] to fine tune the data type used. If the **format** is not
/// supported by the UI it may default back to [`SchemaType`] alone.
/// Format is an open value, so you can use any formats, even not those defined by the
/// OpenAPI Specification.
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "lowercase")]
#[serde(rename_all = "lowercase", untagged)]
pub enum SchemaFormat {
KnownFormat(KnownFormat),
Custom(String),
}

#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
#[serde(rename_all = "lowercase")]
pub enum KnownFormat {
/// 32 bit integer.
Int32,
/// 64 bit integer.
Expand All @@ -1138,7 +1148,7 @@ pub enum SchemaFormat {
/// ISO-8601 full date time [FRC3339](https://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14).
#[serde(rename = "date-time")]
DateTime,
/// Hint to UI to obsucre input.
/// Hint to UI to obscure input.
Password,
/// Used with [`String`] values to indicate value is in UUID format.
///
Expand Down Expand Up @@ -1172,7 +1182,7 @@ mod tests {
"id",
ObjectBuilder::new()
.schema_type(SchemaType::Integer)
.format(Some(SchemaFormat::Int32))
.format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32)))
.description(Some("Id of credential"))
.default(Some(json!(1i32))),
)
Expand Down Expand Up @@ -1340,7 +1350,7 @@ mod tests {
"id",
ObjectBuilder::new()
.schema_type(SchemaType::Integer)
.format(Some(SchemaFormat::Int32))
.format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32)))
.description(Some("Id of credential"))
.default(Some(json!(1i32))),
),
Expand All @@ -1357,7 +1367,7 @@ mod tests {
"id",
ObjectBuilder::new()
.schema_type(SchemaType::Integer)
.format(Some(SchemaFormat::Int32))
.format(Some(SchemaFormat::KnownFormat(KnownFormat::Int32)))
.description(Some("Id of credential"))
.default(Some(json!(1i32))),
),
Expand Down

0 comments on commit 3180d18

Please sign in to comment.