diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index d3203dfd..512a8578 100644 --- a/utoipa-gen/CHANGELOG.md +++ b/utoipa-gen/CHANGELOG.md @@ -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 diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index e743af8e..1ee2bdce 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -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( diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 2fc28f2d..c72c5081 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -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}, @@ -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); @@ -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; diff --git a/utoipa-gen/src/component/schema/enums.rs b/utoipa-gen/src/component/schema/enums.rs index 87908999..b2c9f925 100644 --- a/utoipa-gen/src/component/schema/enums.rs +++ b/utoipa-gen/src/component/schema/enums.rs @@ -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, }, @@ -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::, Diagnostics>>()?; @@ -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()); diff --git a/utoipa-gen/src/component/schema/features.rs b/utoipa-gen/src/component/schema/features.rs index a814d2b7..7db91262 100644 --- a/utoipa-gen/src/component/schema/features.rs +++ b/utoipa-gen/src/component/schema/features.rs @@ -38,7 +38,8 @@ impl Parse for NamedFieldStructFeatures { crate::component::features::attributes::Default, Deprecated, Description, - Bound + Bound, + NoRecursion ))) } } @@ -103,7 +104,8 @@ impl Parse for MixedEnumFeatures { As, Deprecated, Description, - Discriminator + Discriminator, + NoRecursion ))) } } @@ -164,7 +166,8 @@ impl Parse for EnumNamedFieldVariantFeatures { RenameAll, Deprecated, MaxProperties, - MinProperties + MinProperties, + NoRecursion ))) } } diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 800001a8..710a4091 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -134,6 +134,10 @@ static CONFIG: once_cell::sync::Lazy = /// 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(...)]` /// @@ -325,6 +329,10 @@ static CONFIG: once_cell::sync::Lazy = /// * `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 /// @@ -336,7 +344,7 @@ static CONFIG: once_cell::sync::Lazy = /// /// 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(...)`** /// @@ -377,6 +385,10 @@ static CONFIG: once_cell::sync::Lazy = /// 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)]` /// diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index b8abf823..4e0bf12e 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -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, + right: Box, + } + + #[derive(ToSchema)] + #[schema(no_recursion)] + pub enum TreeRecursion { + Named { left: Box }, + Unnamed(Box), + NoValue, + } + + #[derive(ToSchema)] + pub enum Recursion { + #[schema(no_recursion)] + Named { + left: Box, + right: Box, + }, + #[schema(no_recursion)] + Unnamed(Box), + 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}") +}