Skip to content

Commit

Permalink
The attribute now accepts an optional bool value
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Marc Le Roux committed Nov 1, 2024
1 parent 9e85e0e commit f412f8e
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 28 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

* The `#[schema(ignore)]` attribute now accepts an optional bool value (https://github.com/juhaku/utoipa/pull/1177)

## 5.1.3 - Oct 27 2024

### Fixed
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::{LitBoolOrExprCall, 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 LitBoolOrExprCall);
}

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
20 changes: 16 additions & 4 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -122,7 +122,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 +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<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 +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<Rename>)
.map(|rename| rename.into_value());
let rename_to = field_serde_params
Expand Down Expand Up @@ -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 })
}

Expand Down
44 changes: 29 additions & 15 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
as_tokens_or_diagnostics,
component::features::attributes::{Rename, Title, ValueType},
doc_comment::CommentAttributes,
parse_utils::LitBoolOrExprCall,
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<LitBoolOrExprCall>,
}

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,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)
Expand Down Expand Up @@ -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(_)))
Expand Down Expand Up @@ -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)
Expand All @@ -636,6 +649,7 @@ impl NamedStructSchema {
renamed_field: rename_field,
required,
is_option,
ignore,
}))
}
}
Expand Down
62 changes: 59 additions & 3 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ static CONFIG: once_cell::sync::Lazy<utoipa_config::Config> =
/// 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.
///* `ignore` or `ignore = ...` Can be used to skip the field from being serialized to OpenAPI schema. It accepts either a litteral `bool` value
/// or a function reference that returns a `bool` value (`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 litteral `bool` value
/// or a function reference that returns a `bool` value (`Fn() -> bool`).
///
/// #### Field nullability and required rules
///
Expand Down Expand Up @@ -3638,7 +3640,7 @@ mod parse_utils {
parse::{Parse, ParseStream},
punctuated::Punctuated,
token::Comma,
Error, Expr, LitBool, LitStr, Token,
Error, Expr, ExprCall, LitBool, LitStr, Token,
};

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

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

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

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

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

match expr {
Expr::Call(expr_call) => Ok(LitBoolOrExprCall::ExprCall(expr_call)),
_ => Err(syn::Error::new(
proc_macro2::Span::call_site(),
"expected literal bool or function call",
)),
}
}
}
}

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

pub fn parse_next_literal_bool_or_call(input: ParseStream) -> syn::Result<LitBoolOrExprCall> {
if input.peek(Token![=]) {
parse_next(input, || LitBoolOrExprCall::parse(input))
} else {
Ok(LitBoolOrExprCall::from(true))
}
}
}
46 changes: 46 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading

0 comments on commit f412f8e

Please sign in to comment.