Skip to content

Commit

Permalink
Feature openapi 31 (#981)
Browse files Browse the repository at this point in the history
Migrate to OpenAPI 3.1

Implemented nullable schema and Ref supports `description` and `summary`
fields now.

`examples` field has been added to all schema objects. It is tested on
structs. Complex enums do not directly support `examples` but the
`examples` will directly render under the actual type object of
a complex enum variant.

Add support for deriving `content_encoding` and `content_media_type` on
struct named fields.

In OpenAPI 3.0 the request body and response body with type `string` and
format `binary`, `byte`, `base64` was considered a
`application/octet-stream`. However the OpenAPI 3.1 does not require
such and and the schema can be completely ignored in such cases.

This commit will remove the old behaviour where [u8] was rendered as
`type: string` `format: binary` for recognizing
`application/octet-stream`. From now on the content schema will be
resolved as is which means that [u8] will be rendered as `type: array`
of `items: int32`.

Update documentation and add more known formats according to the https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00#section-7.3
  • Loading branch information
juhaku authored Aug 11, 2024
1 parent 92cac85 commit ae6cedd
Show file tree
Hide file tree
Showing 23 changed files with 1,194 additions and 461 deletions.
2 changes: 1 addition & 1 deletion scripts/doc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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,url,actix-web,axum,rocket \
--features actix_extras,openapi_extensions,yaml,uuid,ulid,url,non_strict_integers,actix-web,axum,rocket \
--config 'build.rustdocflags = ["--cfg", "doc_cfg"]'
220 changes: 131 additions & 89 deletions utoipa-gen/src/component.rs

Large diffs are not rendered by default.

133 changes: 129 additions & 4 deletions utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ use std::{fmt::Display, mem, str::FromStr};

use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parenthesized, parse::ParseStream, LitFloat, LitInt, LitStr, TypePath};
use syn::{
parenthesized, parse::ParseStream, punctuated::Punctuated, LitFloat, LitInt, LitStr, Token,
TypePath,
};

use crate::{
as_tokens_or_diagnostics, parse_utils,
path::parameter::{self, ParameterStyle},
schema_type::{SchemaFormat, SchemaType},
AnyValue, Diagnostics, OptionExt, ToTokensDiagnostics,
AnyValue, Array, Diagnostics, OptionExt, ToTokensDiagnostics,
};

use super::{schema, serde::RenameRule, GenericType, TypeTree};
Expand Down Expand Up @@ -84,6 +87,7 @@ pub trait Parse {
#[derive(Clone)]
pub enum Feature {
Example(Example),
Examples(Examples),
Default(Default),
Inline(Inline),
XmlAttr(XmlAttr),
Expand Down Expand Up @@ -118,6 +122,8 @@ pub enum Feature {
As(As),
AdditionalProperties(AdditionalProperties),
Required(Required),
ContentEncoding(ContentEncoding),
ContentMediaType(ContentMediaType),
}

impl Feature {
Expand Down Expand Up @@ -175,12 +181,13 @@ impl ToTokensDiagnostics for Feature {
let feature = match &self {
Feature::Default(default) => quote! { .default(#default) },
Feature::Example(example) => quote! { .example(Some(#example)) },
Feature::Examples(examples) => quote! { .examples(#examples) },
Feature::XmlAttr(xml) => quote! { .xml(Some(#xml)) },
Feature::Format(format) => quote! { .format(Some(#format)) },
Feature::WriteOnly(write_only) => quote! { .write_only(Some(#write_only)) },
Feature::ReadOnly(read_only) => quote! { .read_only(Some(#read_only)) },
Feature::Title(title) => quote! { .title(Some(#title)) },
Feature::Nullable(nullable) => quote! { .nullable(#nullable) },
Feature::Nullable(_nullable) => return Err(Diagnostics::new("Nullable does not support `ToTokens`")),
Feature::Rename(rename) => rename.to_token_stream(),
Feature::Style(style) => quote! { .style(Some(#style)) },
Feature::ParameterIn(parameter_in) => quote! { .parameter_in(#parameter_in) },
Expand Down Expand Up @@ -214,6 +221,8 @@ impl ToTokensDiagnostics for Feature {
Feature::AdditionalProperties(additional_properties) => {
quote! { .additional_properties(Some(#additional_properties)) }
}
Feature::ContentEncoding(content_encoding) => quote! { .content_encoding(#content_encoding) },
Feature::ContentMediaType(content_media_type) => quote! { .content_media_type(#content_media_type) },
Feature::RenameAll(_) => {
return Err(Diagnostics::new("RenameAll feature does not support `ToTokens`"))
}
Expand Down Expand Up @@ -259,6 +268,7 @@ impl Display for Feature {
match self {
Feature::Default(default) => default.fmt(f),
Feature::Example(example) => example.fmt(f),
Feature::Examples(examples) => examples.fmt(f),
Feature::XmlAttr(xml) => xml.fmt(f),
Feature::Format(format) => format.fmt(f),
Feature::WriteOnly(write_only) => write_only.fmt(f),
Expand Down Expand Up @@ -292,6 +302,8 @@ impl Display for Feature {
Feature::As(as_feature) => as_feature.fmt(f),
Feature::AdditionalProperties(additional_properties) => additional_properties.fmt(f),
Feature::Required(required) => required.fmt(f),
Feature::ContentEncoding(content_encoding) => content_encoding.fmt(f),
Feature::ContentMediaType(content_media_type) => content_media_type.fmt(f),
}
}
}
Expand All @@ -301,6 +313,7 @@ impl Validatable for Feature {
match &self {
Feature::Default(default) => default.is_validatable(),
Feature::Example(example) => example.is_validatable(),
Feature::Examples(examples) => examples.is_validatable(),
Feature::XmlAttr(xml) => xml.is_validatable(),
Feature::Format(format) => format.is_validatable(),
Feature::WriteOnly(write_only) => write_only.is_validatable(),
Expand Down Expand Up @@ -336,6 +349,8 @@ impl Validatable for Feature {
additional_properties.is_validatable()
}
Feature::Required(required) => required.is_validatable(),
Feature::ContentEncoding(content_encoding) => content_encoding.is_validatable(),
Feature::ContentMediaType(content_media_type) => content_media_type.is_validatable(),
}
}
}
Expand All @@ -355,6 +370,7 @@ macro_rules! is_validatable {
is_validatable! {
Default => false,
Example => false,
Examples => false,
XmlAttr => false,
Format => false,
WriteOnly => false,
Expand Down Expand Up @@ -387,7 +403,9 @@ is_validatable! {
Deprecated => false,
As => false,
AdditionalProperties => false,
Required => false
Required => false,
ContentEncoding => false,
ContentMediaType => false
}

#[derive(Clone)]
Expand All @@ -414,6 +432,46 @@ impl From<Example> for Feature {

name!(Example = "example");

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Examples(Vec<AnyValue>);

impl Parse for Examples {
fn parse(input: ParseStream, _: Ident) -> syn::Result<Self>
where
Self: std::marker::Sized,
{
let examples;
parenthesized!(examples in input);

Ok(Self(
Punctuated::<AnyValue, Token![,]>::parse_terminated_with(
&examples,
AnyValue::parse_any,
)?
.into_iter()
.collect(),
))
}
}

impl ToTokens for Examples {
fn to_tokens(&self, tokens: &mut TokenStream) {
if !self.0.is_empty() {
let examples = Array::Borrowed(&self.0).to_token_stream();
examples.to_tokens(tokens);
}
}
}

impl From<Examples> for Feature {
fn from(value: Examples) -> Self {
Feature::Examples(value)
}
}

name!(Examples = "examples");

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Default(pub(crate) Option<AnyValue>);
Expand Down Expand Up @@ -666,6 +724,18 @@ impl Nullable {
pub fn new() -> Self {
Self(true)
}

pub fn value(&self) -> bool {
self.0
}

pub fn into_schema_type_token_stream(self) -> proc_macro2::TokenStream {
if self.0 {
quote! {utoipa::openapi::schema::Type::Null}
} else {
proc_macro2::TokenStream::new()
}
}
}

impl Parse for Nullable {
Expand Down Expand Up @@ -1505,6 +1575,60 @@ impl From<Required> for Feature {

name!(Required = "required");

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ContentEncoding(String);

impl Parse for ContentEncoding {
fn parse(input: ParseStream, _: Ident) -> syn::Result<Self>
where
Self: std::marker::Sized,
{
parse_utils::parse_next_literal_str(input).map(Self)
}
}

impl ToTokens for ContentEncoding {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}

name!(ContentEncoding = "content_encoding");

impl From<ContentEncoding> for Feature {
fn from(value: ContentEncoding) -> Self {
Self::ContentEncoding(value)
}
}

#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ContentMediaType(String);

impl Parse for ContentMediaType {
fn parse(input: ParseStream, _: Ident) -> syn::Result<Self>
where
Self: std::marker::Sized,
{
parse_utils::parse_next_literal_str(input).map(Self)
}
}

impl ToTokens for ContentMediaType {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}

impl From<ContentMediaType> for Feature {
fn from(value: ContentMediaType) -> Self {
Self::ContentMediaType(value)
}
}

name!(ContentMediaType = "content_media_type");

pub trait Validator {
fn is_valid(&self) -> Result<(), &'static str>;
}
Expand Down Expand Up @@ -1843,6 +1967,7 @@ macro_rules! impl_feature_into_inner {

impl_feature_into_inner! {
Example,
Examples,
Default,
Inline,
XmlAttr,
Expand Down
9 changes: 5 additions & 4 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ impl<'e> EnumSchema<'e> {
features::parse_schema_features_with(attributes, |input| {
Ok(parse_features!(
input as super::features::Example,
super::features::Examples,
super::features::Default,
super::features::Title,
As
Expand Down Expand Up @@ -1345,7 +1346,7 @@ impl ComplexEnum<'_> {
#title
.item(#unnamed_enum_tokens)
.item(utoipa::openapi::schema::ObjectBuilder::new()
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.schema_type(utoipa::openapi::schema::Type::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
)
Expand All @@ -1354,7 +1355,7 @@ impl ComplexEnum<'_> {
Ok(quote! {
#unnamed_enum_tokens
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.schema_type(utoipa::openapi::schema::Type::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
})
Expand Down Expand Up @@ -1453,7 +1454,7 @@ impl ComplexEnum<'_> {
Ok(quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.schema_type(utoipa::openapi::schema::Type::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
.property(#content, #named_enum_tokens)
Expand Down Expand Up @@ -1498,7 +1499,7 @@ impl ComplexEnum<'_> {
Ok(quote! {
utoipa::openapi::schema::ObjectBuilder::new()
#title
.schema_type(utoipa::openapi::schema::SchemaType::Object)
.schema_type(utoipa::openapi::schema::Type::Object)
.property(#tag, #variant_name_tokens)
.required(#tag)
.property(#content, #unnamed_enum_tokens)
Expand Down
14 changes: 11 additions & 3 deletions utoipa-gen/src/component/schema/enum_variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ pub trait Variant {
/// Get enum variant type. By default enum variant is `string`
fn get_type(&self) -> (TokenStream, TokenStream) {
(
SchemaType(&parse_quote!(str)).to_token_stream(),
SchemaType {
path: &parse_quote!(str),
nullable: false,
}
.to_token_stream(),
quote! {&str},
)
}
Expand Down Expand Up @@ -50,7 +54,11 @@ where

fn get_type(&self) -> (TokenStream, TokenStream) {
(
SchemaType(&self.type_path.path).to_token_stream(),
SchemaType {
path: &self.type_path.path,
nullable: false,
}
.to_token_stream(),
self.type_path.to_token_stream(),
)
}
Expand Down Expand Up @@ -255,7 +263,7 @@ impl ToTokensDiagnostics for UntaggedEnum {

tokens.extend(quote! {
utoipa::openapi::schema::ObjectBuilder::new()
.nullable(true)
.schema_type(utoipa::openapi::schema::Type::Null)
.default(Some(serde_json::Value::Null))
#title
});
Expand Down
Loading

0 comments on commit ae6cedd

Please sign in to comment.