From 2da570c5f9e1081456bbbe2d656953836c301c03 Mon Sep 17 00:00:00 2001 From: Jean-Marc Le Roux Date: Fri, 1 Nov 2024 11:56:37 +0100 Subject: [PATCH] The attribute now accepts an optional bool value --- utoipa-gen/CHANGELOG.md | 6 +++ utoipa-gen/src/component/features.rs | 2 +- .../src/component/features/attributes.rs | 22 ++++++-- utoipa-gen/src/component/into_params.rs | 20 ++++++-- utoipa-gen/src/component/schema.rs | 44 ++++++++++------ utoipa-gen/src/lib.rs | 51 +++++++++++++++++++ utoipa-gen/tests/path_derive.rs | 46 +++++++++++++++++ utoipa-gen/tests/schema_derive_test.rs | 29 +++++++++++ 8 files changed, 195 insertions(+), 25 deletions(-) diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index e8e8dcb8..7fa251ca 100644 --- a/utoipa-gen/CHANGELOG.md +++ b/utoipa-gen/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog - utoipa-gen +## Unreleased + +### Changed + +* The `#[schema(ignore)]` attribute now accepts an optional bool value (https://github.com/juhaku/utoipa/pull/1177) + ## 5.1.3 - Oct 27 2024 ### Fixed diff --git a/utoipa-gen/src/component/features.rs b/utoipa-gen/src/component/features.rs index 661095d4..998f2bda 100644 --- a/utoipa-gen/src/component/features.rs +++ b/utoipa-gen/src/component/features.rs @@ -242,7 +242,7 @@ impl ToTokensDiagnostics for Feature { let name = ::get_name(); quote! { .#name(#required) } } - Feature::Ignore(_) => return Err(Diagnostics::new("Feature::Ignore does not support ToTokens")), + Feature::Ignore(_) => return Err(Diagnostics::new("Ignore does not support `ToTokens`")), }; tokens.extend(feature); diff --git a/utoipa-gen/src/component/features/attributes.rs b/utoipa-gen/src/component/features/attributes.rs index bd05bc8e..cddbecb6 100644 --- a/utoipa-gen/src/component/features/attributes.rs +++ b/utoipa-gen/src/component/features/attributes.rs @@ -9,7 +9,7 @@ use syn::{Error, LitStr, Token, TypePath, WherePredicate}; use crate::component::serde::RenameRule; use crate::component::{schema, GenericType, TypeTree}; -use crate::parse_utils::LitStrOrExpr; +use crate::parse_utils::{LitBoolOrExpr, LitStrOrExpr}; use crate::path::parameter::{self, ParameterStyle}; use crate::schema_type::KnownFormat; use crate::{parse_utils, AnyValue, Array, Diagnostics}; @@ -983,19 +983,25 @@ impl From for Feature { } } -// Nothing to parse, it will be parsed true via `parse_features!` when defined as `ignore` impl_feature! { + /// Ignore feature parsed from macro attributes. #[derive(Clone)] #[cfg_attr(feature = "debug", derive(Debug))] - pub struct Ignore; + pub struct Ignore(pub LitBoolOrExpr); } impl Parse for Ignore { - fn parse(_: ParseStream, _: Ident) -> syn::Result + fn parse(input: syn::parse::ParseStream, _: Ident) -> syn::Result where Self: std::marker::Sized, { - Ok(Self) + parse_utils::parse_next_literal_bool_or_expr(input).map(Self) + } +} + +impl ToTokens for Ignore { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(self.0.to_token_stream()) } } @@ -1005,6 +1011,12 @@ impl From for Feature { } } +impl From for Ignore { + fn from(value: bool) -> Self { + Self(value.into()) + } +} + // Nothing to parse, it is considered to be set when attribute itself is parsed via // `parse_features!`. impl_feature! { diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index c2e461ec..0336227d 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -30,8 +30,8 @@ use crate::{ use super::{ features::{ - impl_into_inner, impl_merge, parse_features, pop_feature, Feature, FeaturesExt, IntoInner, - Merge, ToTokensExt, + attributes, impl_into_inner, impl_merge, parse_features, pop_feature, Feature, FeaturesExt, + IntoInner, Merge, ToTokensExt, }, serde::{self, SerdeContainer, SerdeValue}, ComponentSchema, Container, TypeTree, @@ -122,7 +122,7 @@ impl ToTokensDiagnostics for IntoParams { .collect::, Diagnostics>>()? .into_iter() .filter_map(|(index, field, field_serde_params, field_features)| { - if field_serde_params.skip || field_features.iter().any(|feature| matches!(feature, Feature::Ignore(_))) { + if field_serde_params.skip { None } else { Some((index, field, field_serde_params, field_features)) @@ -158,7 +158,7 @@ impl ToTokensDiagnostics for IntoParams { tokens.extend(quote! { impl #impl_generics utoipa::IntoParams for #ident #ty_generics #where_clause { fn into_params(parameter_in_provider: impl Fn() -> Option) -> Vec { - #params.to_vec() + #params.into_iter().filter(Option::is_some).flatten().collect() } } }); @@ -339,6 +339,7 @@ impl Param { Param::resolve_field_features(field_features, &container_attributes) .map_err(Diagnostics::from)?; + let ignore = pop_feature!(param_features => Feature::Ignore(_)); let rename = pop_feature!(param_features => Feature::Rename(_) as Option) .map(|rename| rename.into_value()); let rename_to = field_serde_params @@ -413,6 +414,17 @@ impl Param { tokens.extend(quote! { .schema(Some(#schema_tokens)).build() }); } + let tokens = match ignore { + Some(Feature::Ignore(attributes::Ignore(bool_or_exp))) => quote! { + if #bool_or_exp { + None + } else { + Some(#tokens) + } + }, + _ => quote! { Some(#tokens) }, + }; + Ok(Self { tokens }) } diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index c72c5081..e90d56ef 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -11,6 +11,7 @@ use crate::{ as_tokens_or_diagnostics, component::features::attributes::{Rename, Title, ValueType}, doc_comment::CommentAttributes, + parse_utils::LitBoolOrExpr, Array, AttributesExt, Diagnostics, OptionExt, ToTokensDiagnostics, }; @@ -24,7 +25,7 @@ use self::{ use super::{ features::{ - attributes::{As, Bound, Description, NoRecursion, RenameAll}, + attributes::{self, As, Bound, Description, NoRecursion, RenameAll}, parse_features, pop_feature, Feature, FeaturesExt, IntoInner, ToTokensExt, }, serde::{self, SerdeContainer, SerdeValue}, @@ -320,6 +321,7 @@ struct NamedStructFieldOptions<'a> { renamed_field: Option>, required: Option, is_option: bool, + ignore: Option, } impl NamedStructSchema { @@ -383,7 +385,7 @@ impl NamedStructSchema { .flatten() .collect::>(); - let mut object_tokens = fields_vec + let object_tokens = fields_vec .iter() .filter(|(_, field_rules, ..)| !field_rules.skip && !field_rules.flatten) .map(|(property, field_rules, field_name, field)| { @@ -398,13 +400,14 @@ impl NamedStructSchema { .collect::, Diagnostics>>()? .into_iter() .fold( - quote! { utoipa::openapi::ObjectBuilder::new() }, + quote! { let mut object = utoipa::openapi::ObjectBuilder::new(); }, |mut object_tokens, ( NamedStructFieldOptions { renamed_field, required, is_option, + ignore, .. }, field_rules, @@ -425,9 +428,9 @@ impl NamedStructSchema { super::rename::(field_name.borrow(), rename_to, rename_all) .unwrap_or(Cow::Borrowed(field_name.borrow())); - object_tokens.extend(quote! { - .property(#name, #field_schema) - }); + let mut property_tokens = quote! { + object = object.property(#name, #field_schema) + }; let component_required = !is_option && super::is_required(field_rules, &container_rules); let required = match (required, component_required) { @@ -436,15 +439,28 @@ impl NamedStructSchema { }; if required { - object_tokens.extend(quote! { + property_tokens.extend(quote! { .required(#name) }) } + object_tokens.extend(match ignore { + Some(bool_or_exp) => quote! { + if !#bool_or_exp { + #property_tokens; + } + }, + None => quote! { #property_tokens; }, + }); + object_tokens }, ); + let mut object_tokens = quote! { + { #object_tokens; object } + }; + let flatten_fields = fields_vec .iter() .filter(|(_, field_rules, ..)| field_rules.flatten) @@ -549,14 +565,6 @@ impl NamedStructSchema { .into_inner() .unwrap_or_default(); - if field_features - .iter() - .any(|feature| matches!(feature, Feature::Ignore(_))) - { - // skip ignored field - return Ok(None); - }; - if features .iter() .any(|feature| matches!(feature, Feature::NoRecursion(_))) @@ -614,6 +622,11 @@ impl NamedStructSchema { let is_option = type_tree.is_option(); + let ignore = match pop_feature!(field_features => Feature::Ignore(_)) { + Some(Feature::Ignore(attributes::Ignore(bool_or_exp))) => Some(bool_or_exp), + _ => None, + }; + Ok(Some(NamedStructFieldOptions { property: if let Some(schema_with) = schema_with { Property::SchemaWith(schema_with) @@ -636,6 +649,7 @@ impl NamedStructSchema { renamed_field: rename_field, required, is_option, + ignore, })) } } diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 710a4091..ac4a89c9 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -3793,4 +3793,55 @@ mod parse_utils { )) } } + + #[cfg_attr(feature = "debug", derive(Debug))] + #[derive(Clone)] + pub enum LitBoolOrExpr { + LitBool(LitBool), + Expr(Expr), + } + + impl From for LitBoolOrExpr { + fn from(value: bool) -> Self { + Self::LitBool(LitBool::new(value, proc_macro2::Span::call_site())) + } + } + + impl Default for LitBoolOrExpr { + fn default() -> Self { + Self::LitBool(LitBool::new(false, proc_macro2::Span::call_site())) + } + } + + impl Parse for LitBoolOrExpr { + fn parse(input: ParseStream) -> syn::Result { + if input.peek(LitBool) { + Ok(LitBoolOrExpr::LitBool(input.parse::()?)) + } else { + Ok(LitBoolOrExpr::Expr(input.parse::()?)) + } + } + } + + impl ToTokens for LitBoolOrExpr { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::LitBool(bool) => bool.to_tokens(tokens), + Self::Expr(expr) => expr.to_tokens(tokens), + } + } + } + + pub fn parse_next_literal_bool_or_expr(input: ParseStream) -> syn::Result { + if input.peek(Token![=]) { + parse_next(input, || LitBoolOrExpr::parse(input)).map_err(|error| { + syn::Error::new( + error.span(), + format!("expected literal bool or expression argument: {error}"), + ) + }) + } else { + Ok(LitBoolOrExpr::from(true)) + } + } } diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index a3380c7a..ea5f9b22 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -2898,6 +2898,52 @@ fn derive_into_params_with_ignored_field() { ) } +#[test] +fn derive_into_params_with_ignored_eq_false_field() { + #![allow(unused)] + + #[derive(IntoParams)] + #[into_params(parameter_in = Query)] + struct Params { + name: String, + #[param(ignore = false)] + __this_is_private: String, + } + + #[utoipa::path(get, path = "/params", params(Params))] + #[allow(unused)] + fn get_params() {} + let operation = test_api_fn_doc! { + get_params, + operation: get, + path: "/params" + }; + + let value = operation.pointer("/parameters"); + + assert_json_eq!( + value, + json!([ + { + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "__this_is_private", + "required": true, + "schema": { + "type": "string" + } + } + ]) + ) +} + #[test] fn derive_octet_stream_request_body() { #![allow(dead_code)] diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 54506a06..b9cf2418 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -5713,6 +5713,35 @@ fn derive_schema_with_ignored_field() { ) } +#[test] +fn derive_schema_with_ignore_eq_false_field() { + #![allow(unused)] + + let value = api_doc! { + struct SchemaIgnoredField { + value: String, + #[schema(ignore = false)] + this_is_not_private: String, + } + }; + + assert_json_eq!( + value, + json!({ + "properties": { + "value": { + "type": "string" + }, + "this_is_not_private": { + "type": "string" + } + }, + "required": [ "value", "this_is_not_private" ], + "type": "object" + }) + ) +} + #[test] fn derive_schema_unnamed_title() { #![allow(unused)]