Skip to content

Commit

Permalink
The #[schema(ignore)] attribute now accepts an optional bool value/fu…
Browse files Browse the repository at this point in the history
…nction path (#1177)

Co-authored-by: Jean-Marc Le Roux <[email protected]>
  • Loading branch information
JMLX42 and Jean-Marc Le Roux authored Nov 1, 2024
1 parent ca643ef commit a792520
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 30 deletions.
1 change: 1 addition & 0 deletions utoipa-gen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Changed

* Added missing formats for `KnownFormat` parsing (https://github.com/juhaku/utoipa/pull/1178)
* The `#[schema(ignore)]` attribute now accepts an optional bool value/function path (https://github.com/juhaku/utoipa/pull/1177)

## 5.1.3 - Oct 27 2024

Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/src/component/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ impl ToTokensDiagnostics for Feature {
let name = <attributes::Required as FeatureLike>::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);
Expand Down
22 changes: 17 additions & 5 deletions utoipa-gen/src/component/features/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{LitBoolOrExprPath, LitStrOrExpr};
use crate::path::parameter::{self, ParameterStyle};
use crate::schema_type::KnownFormat;
use crate::{parse_utils, AnyValue, Array, Diagnostics};
Expand Down Expand Up @@ -983,19 +983,25 @@ impl From<Bound> 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 LitBoolOrExprPath);
}

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

impl ToTokens for Ignore {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(self.0.to_token_stream())
}
}

Expand All @@ -1005,6 +1011,12 @@ impl From<Ignore> for Feature {
}
}

impl From<bool> 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! {
Expand Down
30 changes: 27 additions & 3 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::borrow::Cow;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
parse::Parse, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Field,
Generics, Ident,
Expand All @@ -25,6 +25,7 @@ use crate::{
FieldRename,
},
doc_comment::CommentAttributes,
parse_utils::LitBoolOrExprPath,
Array, Diagnostics, OptionExt, Required, ToTokensDiagnostics,
};

Expand Down Expand Up @@ -122,7 +123,7 @@ impl ToTokensDiagnostics for IntoParams {
.collect::<Result<Vec<_>, 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))
Expand Down Expand Up @@ -158,7 +159,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<utoipa::openapi::path::ParameterIn>) -> Vec<utoipa::openapi::path::Parameter> {
#params.to_vec()
#params.into_iter().filter(Option::is_some).flatten().collect()
}
}
});
Expand Down Expand Up @@ -339,6 +340,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<Rename>)
.map(|rename| rename.into_value());
let rename_to = field_serde_params
Expand Down Expand Up @@ -413,6 +415,28 @@ impl Param {
tokens.extend(quote! { .schema(Some(#schema_tokens)).build() });
}

let tokens = match ignore {
Some(Feature::Ignore(Ignore(LitBoolOrExprPath::LitBool(bool)))) => {
quote_spanned! {
bool.span() => if #bool {
None
} else {
Some(#tokens)
}
}
}
Some(Feature::Ignore(Ignore(LitBoolOrExprPath::ExprPath(path)))) => {
quote_spanned! {
path.span() => if #path() {
None
} else {
Some(#tokens)
}
}
}
_ => quote! { Some(#tokens) },
};

Ok(Self { tokens })
}

Expand Down
51 changes: 35 additions & 16 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::borrow::{Borrow, Cow};

use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, Data, Field,
Fields, FieldsNamed, FieldsUnnamed, Generics, Variant,
Expand All @@ -11,6 +11,7 @@ use crate::{
as_tokens_or_diagnostics,
component::features::attributes::{Rename, Title, ValueType},
doc_comment::CommentAttributes,
parse_utils::LitBoolOrExprPath,
Array, AttributesExt, Diagnostics, OptionExt, ToTokensDiagnostics,
};

Expand All @@ -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},
Expand Down Expand Up @@ -320,6 +321,7 @@ struct NamedStructFieldOptions<'a> {
renamed_field: Option<Cow<'a, str>>,
required: Option<super::features::attributes::Required>,
is_option: bool,
ignore: Option<LitBoolOrExprPath>,
}

impl NamedStructSchema {
Expand Down Expand Up @@ -383,7 +385,7 @@ impl NamedStructSchema {
.flatten()
.collect::<Vec<_>>();

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)| {
Expand All @@ -398,13 +400,14 @@ impl NamedStructSchema {
.collect::<Result<Vec<_>, 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,
Expand All @@ -425,9 +428,9 @@ impl NamedStructSchema {
super::rename::<FieldRename>(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) {
Expand All @@ -436,15 +439,33 @@ impl NamedStructSchema {
};

if required {
object_tokens.extend(quote! {
property_tokens.extend(quote! {
.required(#name)
})
}

object_tokens.extend(match ignore {
Some(LitBoolOrExprPath::LitBool(bool)) => quote_spanned! {
bool.span() => if !#bool {
#property_tokens;
}
},
Some(LitBoolOrExprPath::ExprPath(path)) => quote_spanned! {
path.span() => if !#path() {
#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)
Expand Down Expand Up @@ -549,14 +570,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(_)))
Expand Down Expand Up @@ -614,6 +627,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)
Expand All @@ -636,6 +654,7 @@ impl NamedStructSchema {
renamed_field: rename_field,
required,
is_option,
ignore,
}))
}
}
Expand Down
70 changes: 65 additions & 5 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,9 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// * `content_encoding = ...` Can be used to define content encoding used for underlying schema object.
/// See [`Object::content_encoding`][schema_object_encoding]
/// * `content_media_type = ...` Can be used to define MIME type of a string for underlying schema object.
/// See [`Object::content_media_type`][schema_object_media_type]
///* `ignore` Can be used to skip the field from being serialized to OpenAPI schema.
/// See [`Object::content_media_type`][schema_object_`media_type]
///* `ignore` or `ignore = ...` Can be used to skip the field from being serialized to OpenAPI schema. It accepts either a literal `bool` value
/// or a path to a function that returns `bool` (`Fn() -> bool`).
///* `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**.
Expand Down Expand Up @@ -2349,7 +2350,8 @@ pub fn openapi(input: TokenStream) -> TokenStream {
/// Free form type enables use of arbitrary types within map values.
/// Supports formats _`additional_properties`_ and _`additional_properties = true`_.
///
/// * `ignore` Can be used to skip the field from being serialized to OpenAPI schema.
/// * `ignore` or `ignore = ...` Can be used to skip the field from being serialized to OpenAPI schema. It accepts either a literal `bool` value
/// or a path to a function that returns `bool` (`Fn() -> bool`).
///
/// #### Field nullability and required rules
///
Expand Down Expand Up @@ -3632,13 +3634,14 @@ mod parse_utils {
use std::fmt::Display;

use proc_macro2::{Group, Ident, TokenStream};
use quote::ToTokens;
use quote::{quote, ToTokens};
use syn::{
parenthesized,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
Error, Expr, LitBool, LitStr, Token,
Error, Expr, ExprPath, LitBool, LitStr, Token,
};

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -3793,4 +3796,61 @@ mod parse_utils {
))
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Clone)]
pub enum LitBoolOrExprPath {
LitBool(LitBool),
ExprPath(ExprPath),
}

impl From<bool> for LitBoolOrExprPath {
fn from(value: bool) -> Self {
Self::LitBool(LitBool::new(value, proc_macro2::Span::call_site()))
}
}

impl Default for LitBoolOrExprPath {
fn default() -> Self {
Self::LitBool(LitBool::new(false, proc_macro2::Span::call_site()))
}
}

impl Parse for LitBoolOrExprPath {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitBool) {
Ok(LitBoolOrExprPath::LitBool(input.parse::<LitBool>()?))
} else {
let expr = input.parse::<Expr>()?;

match expr {
Expr::Path(expr_path) => Ok(LitBoolOrExprPath::ExprPath(expr_path)),
_ => Err(syn::Error::new(
expr.span(),
format!(
"expected literal bool or path to a function that returns bool, found: {}",
quote! {#expr}
),
)),
}
}
}
}

impl ToTokens for LitBoolOrExprPath {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::LitBool(bool) => bool.to_tokens(tokens),
Self::ExprPath(call) => call.to_tokens(tokens),
}
}
}

pub fn parse_next_literal_bool_or_call(input: ParseStream) -> syn::Result<LitBoolOrExprPath> {
if input.peek(Token![=]) {
parse_next(input, || LitBoolOrExprPath::parse(input))
} else {
Ok(LitBoolOrExprPath::from(true))
}
}
}
Loading

0 comments on commit a792520

Please sign in to comment.