Skip to content

Commit

Permalink
Make Option non-required & add required attr (#530)
Browse files Browse the repository at this point in the history
Make `Option` fields non-required by default. This is the way it used
to be originally but was changed due hopes for better client code
generation. However since it caused issues with client side generation
the behaviour is now changed back on this commit.

The nullability behaviour will still stay to same, so `Option` will
still be considered nullable. These rules are explained in the docs for
greater detail.

This commit also adds `required` attribute that can be used to declare a
field of `ToSchema` and `IntoParams` as required in order to change the
default required status of a field / parameter.

Since `Option` is by default non-required, this would enforce the `name`
parameter to be required.
```rust
 #[derive(IntoParams)]
 #[into_params(parameter_in = Query)]
 struct Params {
     #[param(required)]
     name: Option<String>,
 }
```
  • Loading branch information
juhaku authored Mar 19, 2023
1 parent 8379322 commit 61046d1
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 121 deletions.
5 changes: 5 additions & 0 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ impl<'t> TypeTree<'t> {
pub fn is_object(&self) -> bool {
self.is("Object")
}

/// Check whether the [`TypeTree`]'s `generic_type` is [`GenericType::Option`]
pub fn is_option(&self) -> bool {
matches!(self.generic_type, Some(GenericType::Option))
}
}

#[cfg(not(feature = "debug"))]
Expand Down
59 changes: 58 additions & 1 deletion utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ pub enum Feature {
Deprecated(Deprecated),
As(As),
AdditionalProperties(AdditionalProperites),
Required(Required),
}

impl Feature {
Expand Down Expand Up @@ -241,6 +242,10 @@ impl ToTokens for Feature {
Feature::As(_) => {
abort!(Span::call_site(), "As does not support `ToTokens`")
}
Feature::Required(required) => {
let name = <Required as Name>::get_name();
quote! { .#name(#required) }
}
};

tokens.extend(feature)
Expand Down Expand Up @@ -284,6 +289,7 @@ impl Display for Feature {
Feature::Deprecated(deprecated) => deprecated.fmt(f),
Feature::As(as_feature) => as_feature.fmt(f),
Feature::AdditionalProperties(additional_properties) => additional_properties.fmt(f),
Feature::Required(required) => required.fmt(f),
}
}
}
Expand Down Expand Up @@ -327,6 +333,7 @@ impl Validatable for Feature {
Feature::AdditionalProperties(additional_properites) => {
additional_properites.is_validatable()
}
Feature::Required(required) => required.is_validatable(),
}
}
}
Expand Down Expand Up @@ -377,7 +384,8 @@ is_validatable! {
Description => false,
Deprecated => false,
As => false,
AdditionalProperites => false
AdditionalProperites => false,
Required => false
}

#[derive(Clone)]
Expand Down Expand Up @@ -1417,6 +1425,55 @@ impl From<AdditionalProperites> for Feature {
}
}

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

impl Required {
pub fn is_true(&self) -> bool {
self.0 == true
}
}

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

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

impl From<crate::Required> for Required {
fn from(value: crate::Required) -> Self {
if value == crate::Required::True {
Self(true)
} else {
Self(false)
}
}
}

impl From<bool> for Required {
fn from(value: bool) -> Self {
Self(value)
}
}

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

name!(Required = "required");

pub trait Validator {
fn is_valid(&self) -> Result<(), &'static str>;
}
Expand Down
15 changes: 11 additions & 4 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use crate::{

use super::{
features::{
impl_into_inner, impl_merge, parse_features, pop_feature, Feature, FeaturesExt, IntoInner,
Merge, ToTokensExt,
impl_into_inner, impl_merge, parse_features, pop_feature, pop_feature_as_inner, Feature,
FeaturesExt, IntoInner, Merge, ToTokensExt,
},
serde::{self, SerdeContainer},
ComponentSchema, TypeTree,
Expand Down Expand Up @@ -228,6 +228,7 @@ impl Parse for FieldFeatures {
Example,
Explode,
SchemaWith,
component::features::Required,
// param schema features
Inline,
Format,
Expand Down Expand Up @@ -399,8 +400,14 @@ impl ToTokens for Param<'_> {
.map(|value_type| value_type.as_type_tree())
.unwrap_or(type_tree);

let required: Required =
component::is_required(field_param_serde.as_ref(), self.serde_container).into();
let required = pop_feature_as_inner!(param_features => Feature::Required(_v))
.as_ref()
.map(super::features::Required::is_true)
.unwrap_or(false);

let non_required = (component.is_option() && !required)
|| !component::is_required(field_param_serde.as_ref(), self.serde_container);
let required: Required = (!non_required).into();

tokens.extend(quote! {
.required(#required)
Expand Down
107 changes: 71 additions & 36 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,18 @@ pub struct NamedStructSchema<'a> {
pub schema_as: Option<As>,
}

struct NamedStructFieldOptions<'a> {
property: Property,
rename_field_value: Option<Cow<'a, str>>,
required: Option<super::features::Required>,
is_option: bool,
}

impl NamedStructSchema<'_> {
fn field_as_schema_property<R>(
&self,
field: &Field,
yield_: impl FnOnce(Property, Option<Cow<'_, str>>) -> R,
yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R,
) -> R {
let type_tree = &mut TypeTree::from_type(&field.ty);

Expand Down Expand Up @@ -305,21 +312,26 @@ impl NamedStructSchema<'_> {
.map(|value_type| value_type.as_type_tree());
let comments = CommentAttributes::from_attributes(&field.attrs);
let with_schema = pop_feature!(field_features => Feature::SchemaWith(_));
let required = pop_feature_as_inner!(field_features => Feature::Required(_v));
let type_tree = override_type_tree.as_ref().unwrap_or(type_tree);
let is_option = type_tree.is_option();

yield_(
if let Some(with_schema) = with_schema {
yield_(NamedStructFieldOptions {
property: if let Some(with_schema) = with_schema {
Property::WithSchema(with_schema)
} else {
Property::Schema(ComponentSchema::new(super::ComponentSchemaProps {
type_tree: override_type_tree.as_ref().unwrap_or(type_tree),
type_tree,
features: field_features,
description: Some(&comments),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
}))
},
rename_field,
)
rename_field_value: rename_field,
required,
is_option,
})
}
}

Expand Down Expand Up @@ -348,37 +360,57 @@ impl ToTokens for NamedStructSchema<'_> {
field_name = &field_name[2..];
}

self.field_as_schema_property(field, |property, rename| {
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| field_rule.rename.as_deref().map(Cow::Borrowed))
.or(rename);
let rename_all = container_rules
.as_ref()
.and_then(|container_rule| container_rule.rename_all.as_ref())
.or_else(|| {
self.rename_all
.as_ref()
.map(|rename_all| rename_all.as_rename_rule())
self.field_as_schema_property(
field,
|NamedStructFieldOptions {
property,
rename_field_value,
required,
is_option,
}| {
let rename_to = field_rule
.as_ref()
.and_then(|field_rule| {
field_rule.rename.as_deref().map(Cow::Borrowed)
})
.or(rename_field_value);
let rename_all = container_rules
.as_ref()
.and_then(|container_rule| container_rule.rename_all.as_ref())
.or_else(|| {
self.rename_all
.as_ref()
.map(|rename_all| rename_all.as_rename_rule())
});

let name =
super::rename::<FieldRename>(field_name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(field_name));

object_tokens.extend(quote! {
.property(#name, #property)
});

let name = super::rename::<FieldRename>(field_name, rename_to, rename_all)
.unwrap_or(Cow::Borrowed(field_name));

object_tokens.extend(quote! {
.property(#name, #property)
});

if let Property::Schema(_) = property {
if super::is_required(field_rule.as_ref(), container_rules.as_ref()) {
object_tokens.extend(quote! {
.required(#name)
})
if let Property::Schema(_) = property {
if (!is_option
&& super::is_required(
field_rule.as_ref(),
container_rules.as_ref(),
))
|| required
.as_ref()
.map(super::features::Required::is_true)
.unwrap_or(false)
{
object_tokens.extend(quote! {
.required(#name)
})
}
}
}

object_tokens
})
object_tokens
},
)
},
);

Expand All @@ -397,9 +429,12 @@ impl ToTokens for NamedStructSchema<'_> {
});

for field in flatten_fields {
self.field_as_schema_property(field, |schema_property, _| {
tokens.extend(quote! { .item(#schema_property) });
})
self.field_as_schema_property(
field,
|NamedStructFieldOptions { property, .. }| {
tokens.extend(quote! { .item(#property) });
},
)
}

tokens.extend(quote! {
Expand Down
7 changes: 4 additions & 3 deletions utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::component::features::{
impl_into_inner, impl_merge, parse_features, AdditionalProperites, As, Default, Example,
ExclusiveMaximum, ExclusiveMinimum, Feature, Format, Inline, IntoInner, MaxItems, MaxLength,
MaxProperties, Maximum, Merge, MinItems, MinLength, MinProperties, Minimum, MultipleOf,
Nullable, Pattern, ReadOnly, Rename, RenameAll, SchemaWith, Title, ValueType, WriteOnly,
XmlAttr,
Nullable, Pattern, ReadOnly, Rename, RenameAll, Required, SchemaWith, Title, ValueType,
WriteOnly, XmlAttr,
};

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -106,7 +106,8 @@ impl Parse for NamedFieldFeatures {
MaxItems,
MinItems,
SchemaWith,
AdditionalProperites
AdditionalProperites,
Required
)))
}
}
Expand Down
Loading

0 comments on commit 61046d1

Please sign in to comment.