Skip to content

Commit

Permalink
Enhance no_recursion rule to apply also containers (#1144)
Browse files Browse the repository at this point in the history
This commit further enhances the `no_recursion` rule to also apply on
named structs, enums and named field enum variants. When provided on
these aforementioned levels it will apply to its fields / variants.

Example of the enhanced syntax.
```rust
 #[derive(ToSchema)]
 #[schema(no_recursion)]
 pub struct Tree {
     left: Box<Tree>,
     right: Box<Tree>,
 }

 #[derive(ToSchema)]
 #[schema(no_recursion)]
 pub enum TreeRecursion {
     Named { left: Box<TreeRecursion> },
     Unnamed(Box<TreeRecursion>),
     NoValue,
 }

 #[derive(ToSchema)]
 pub enum Recursion {
     #[schema(no_recursion)]
     Named {
         left: Box<Recursion>,
         right: Box<Recursion>,
     },
     #[schema(no_recursion)]
     Unnamed(Box<Recursion>),
     NoValue,
 }
```

Closes #1137
  • Loading branch information
juhaku authored Oct 16, 2024
1 parent 7073395 commit 7e13bc9
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 12 deletions.
6 changes: 6 additions & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog - utoipa-gen

## Unreleased

### Changed

* Enhance no_recursion rule to apply also containers (https://github.com/juhaku/utoipa/pull/1144)

## 5.1.0 - Oct 16 2024

### Added
Expand Down
10 changes: 6 additions & 4 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1188,10 +1188,12 @@ impl ComponentSchema {
let type_path = &**type_tree.path.as_ref().unwrap();
let rewritten_path = type_path.rewrite_path()?;
let nullable_item = nullable_one_of_item(nullable);
let mut object_schema_reference = SchemaReference::default();
object_schema_reference.no_recursion = features
.iter()
.any(|feature| matches!(feature, Feature::NoRecursion(_)));
let mut object_schema_reference = SchemaReference {
no_recursion: features
.iter()
.any(|feature| matches!(feature, Feature::NoRecursion(_))),
..SchemaReference::default()
};

if let Some(children) = &type_tree.children {
let children_name = Self::compose_name(
Expand Down
10 changes: 9 additions & 1 deletion utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use self::{

use super::{
features::{
attributes::{As, Bound, Description, RenameAll},
attributes::{As, Bound, Description, NoRecursion, RenameAll},
parse_features, pop_feature, Feature, FeaturesExt, IntoInner, ToTokensExt,
},
serde::{self, SerdeContainer, SerdeValue},
Expand Down Expand Up @@ -515,6 +515,7 @@ impl NamedStructSchema {
features.push(Feature::Deprecated(true.into()));
}

let _ = pop_feature!(features => Feature::NoRecursion(_));
tokens.extend(features.to_token_stream()?);

let comments = CommentAttributes::from_attributes(root.attributes);
Expand Down Expand Up @@ -556,6 +557,13 @@ impl NamedStructSchema {
return Ok(None);
};

if features
.iter()
.any(|feature| matches!(feature, Feature::NoRecursion(_)))
{
field_features.push(Feature::NoRecursion(NoRecursion));
}

let schema_default = features.iter().any(|f| matches!(f, Feature::Default(_)));
let serde_default = container_rules.default;

Expand Down
14 changes: 11 additions & 3 deletions utoipa-gen/src/component/schema/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use crate::{
component::{
features::{
attributes::{
Deprecated, Description, Discriminator, Example, Examples, Rename, RenameAll, Title,
Deprecated, Description, Discriminator, Example, Examples, NoRecursion, Rename,
RenameAll, Title,
},
parse_features, pop_feature, Feature, IntoInner, IsInline, ToTokensExt,
},
Expand Down Expand Up @@ -334,14 +335,20 @@ impl<'p> MixedEnum<'p> {

let mut items = variants
.into_iter()
.map(|(variant, variant_serde_rules, features)| {
.map(|(variant, variant_serde_rules, mut variant_features)| {
if features
.iter()
.any(|feature| matches!(feature, Feature::NoRecursion(_)))
{
variant_features.push(Feature::NoRecursion(NoRecursion));
}
MixedEnumContent::new(
variant,
root,
&container_rules,
rename_all.as_ref(),
variant_serde_rules,
features,
variant_features,
)
})
.collect::<Result<Vec<MixedEnumContent>, Diagnostics>>()?;
Expand All @@ -356,6 +363,7 @@ impl<'p> MixedEnum<'p> {
discriminator,
};

let _ = pop_feature!(features => Feature::NoRecursion(_));
let mut tokens = one_of_enum.to_token_stream();
tokens.extend(features.to_token_stream());

Expand Down
9 changes: 6 additions & 3 deletions utoipa-gen/src/component/schema/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ impl Parse for NamedFieldStructFeatures {
crate::component::features::attributes::Default,
Deprecated,
Description,
Bound
Bound,
NoRecursion
)))
}
}
Expand Down Expand Up @@ -103,7 +104,8 @@ impl Parse for MixedEnumFeatures {
As,
Deprecated,
Description,
Discriminator
Discriminator,
NoRecursion
)))
}
}
Expand Down Expand Up @@ -164,7 +166,8 @@ impl Parse for EnumNamedFieldVariantFeatures {
RenameAll,
Deprecated,
MaxProperties,
MinProperties
MinProperties,
NoRecursion
)))
}
}
Expand Down
14 changes: 13 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// contain. Value must be a number.
/// * `min_properties = ...` Can be used to define minimum number of properties this struct can
/// contain. Value must be a number.
///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` ->
/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow
/// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On
/// struct level the _`no_recursion`_ rule will be applied to all of its fields.
///
/// ## Named Fields Optional Configuration Options for `#[schema(...)]`
///
Expand Down Expand Up @@ -325,6 +329,10 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// * `discriminator = ...` or `discriminator(...)` Can be used to define OpenAPI discriminator
/// field for enums with single unnamed _`ToSchema`_ reference field. See the [discriminator
/// syntax][derive@ToSchema#schemadiscriminator-syntax].
///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` ->
/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow
/// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On
/// enum level the _`no_recursion`_ rule will be applied to all of its variants.
///
/// ### `#[schema(discriminator)]` syntax
///
Expand All @@ -336,7 +344,7 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
///
/// Can be literal string or expression e.g. [_`const`_][const] reference. It can be defined as
/// _`discriminator = "value"`_ where the assigned value is the
/// discriminator field that must exists in each variant referencing schema.
/// discriminator field that must exists in each variant referencing schema.
///
/// **Complex form `discriminator(...)`**
///
Expand Down Expand Up @@ -377,6 +385,10 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// contain. Value must be a number.
/// * `min_properties = ...` Can be used to define minimum number of properties this struct can
/// contain. Value must be a number.
///* `no_recursion` Is used to break from recursion in case of looping schema tree e.g. `Pet` ->
/// `Owner` -> `Pet`. _`no_recursion`_ attribute must be used within `Ower` type not to allow
/// recurring into `Pet`. Failing to do so will cause infinite loop and runtime **panic**. On
/// named field variant level the _`no_recursion`_ rule will be applied to all of its fields.
///
/// ## Mixed Enum Unnamed Field Variant Optional Configuration Options for `#[serde(schema)]`
///
Expand Down
41 changes: 41 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5960,3 +5960,44 @@ fn test_recursion_compiles() {
.expect("OpenApi is JSON serializable");
println!("{json}")
}

#[test]
fn test_named_and_enum_container_recursion_compiles() {
#![allow(unused)]

#[derive(ToSchema)]
#[schema(no_recursion)]
pub struct Tree {
left: Box<Tree>,
right: Box<Tree>,
}

#[derive(ToSchema)]
#[schema(no_recursion)]
pub enum TreeRecursion {
Named { left: Box<TreeRecursion> },
Unnamed(Box<TreeRecursion>),
NoValue,
}

#[derive(ToSchema)]
pub enum Recursion {
#[schema(no_recursion)]
Named {
left: Box<Recursion>,
right: Box<Recursion>,
},
#[schema(no_recursion)]
Unnamed(Box<Recursion>),
NoValue,
}

#[derive(OpenApi)]
#[openapi(components(schemas(Recursion, Tree, TreeRecursion)))]
pub struct ApiDoc {}

let json = ApiDoc::openapi()
.to_pretty_json()
.expect("OpenApi is JSON serializable");
println!("{json}")
}

0 comments on commit 7e13bc9

Please sign in to comment.