diff --git a/utoipa-gen/CHANGELOG.md b/utoipa-gen/CHANGELOG.md index e8e8dcb8..a483469f 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 + ## 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..f5b6abab 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(ignore) => quote! { .ignore(Some(#ignore)) }, }; 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..aaecd5ec 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)) @@ -366,6 +366,20 @@ impl Param { }, ); + // tokens.extend( + // if let Some(ignore) = pop_feature!(param_features => Feature::Ignore(_)) { + // let ignore = ignore.to_token_stream(); + + // quote! { + // //.ignore(Some(#ignore)) + // } + // } else { + // quote! { + // //.ignore(Some(false)) + // } + // }, + // ); + if let Some(deprecated) = super::get_deprecated(&field.attrs) { tokens.extend(quote! { .deprecated(Some(#deprecated)) }); } diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index c72c5081..58716ce7 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -549,14 +549,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(_))) 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..d2f15b0b 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_alse_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..ff489ed4 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -5713,6 +5713,32 @@ fn derive_schema_with_ignored_field() { ) } +#[test] +fn derive_schema_with_ignored_eq_false_field() { + #![allow(unused)] + + let value = api_doc! { + struct SchemaIgnoredField { + value: String, + #[schema(ignore = false)] + __this_is_private: String, + } + }; + + assert_json_eq!( + value, + json!({ + "properties": { + "value": { + "type": "string" + } + }, + "required": [ "value" ], + "type": "object" + }) + ) +} + #[test] fn derive_schema_unnamed_title() { #![allow(unused)] diff --git a/utoipa/src/openapi/path.rs b/utoipa/src/openapi/path.rs index 79bac8af..b3b98ac6 100644 --- a/utoipa/src/openapi/path.rs +++ b/utoipa/src/openapi/path.rs @@ -739,6 +739,10 @@ builder! { /// Optional extensions "x-something". #[serde(skip_serializing_if = "Option::is_none", flatten)] pub extensions: Option, + + /// Describes if [`Parameter`] is ignored. + #[serde(skip)] + pub ignore: Option, } } @@ -815,6 +819,13 @@ impl ParameterBuilder { pub fn extensions(mut self, extensions: Option) -> Self { set_value!(self extensions extensions) } + + /// Define whether [`Parameter`]s are ignored or not. + pub fn ignore(mut self, ignore: Option) -> Self { + self.ignore = ignore; + + self + } } /// In definition of [`Parameter`]. diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 823a8698..5f03cbc3 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -1067,6 +1067,9 @@ builder! { /// See more details at #[serde(skip_serializing_if = "String::is_empty", default)] pub content_media_type: String, + + #[serde(skip)] + pub ignore: Option, } } @@ -1289,6 +1292,10 @@ impl ObjectBuilder { set_value!(self content_media_type content_media_type.into()) } + pub fn ignore(mut self, ignore: Option) -> Self { + set_value!(self ignore ignore) + } + to_array_builder!(); }