From 5fb25fa40fa00c33a326226e2e6ab0bffcdfc8f1 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Sun, 20 Aug 2023 17:10:04 +0300 Subject: [PATCH] Add support for serde skip in `IntoParams` derive (#743) Add support for serde's `skip` attribute in `IntoParams` derive macro. This allows users to use the serde's `skip`, `skip_serializing` or `skip_deserializing` attribute to ignore the field being added as a parameter to a OpenAPI documentation. ```rust #[derive(IntoParams, Serialize)] #[into_params(parameter_in = Query)] #[allow(unused)] struct Params { name: String, name2: Option, #[serde(skip)] name3: Option, } ``` --- utoipa-gen/src/component/into_params.rs | 22 +++- utoipa-gen/src/component/serde.rs | 6 +- utoipa-gen/src/lib.rs | 4 + utoipa-gen/tests/path_derive.rs | 142 ++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 7 deletions(-) diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index 0769fc09..21702cfc 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -27,7 +27,7 @@ use super::{ impl_into_inner, impl_merge, parse_features, pop_feature, pop_feature_as_inner, Feature, FeaturesExt, IntoInner, Merge, ToTokensExt, }, - serde::{self, SerdeContainer}, + serde::{self, SerdeContainer, SerdeValue}, ComponentSchema, TypeTree, }; @@ -104,9 +104,18 @@ impl ToTokens for IntoParams { let params = self .get_struct_fields(&names.as_ref()) .enumerate() - .map(|(index, field)| { + .filter_map(|(index, field)| { + let field_params = serde::parse_value(&field.attrs); + if matches!(&field_params, Some(params) if !params.skip) { + Some((index, field, field_params)) + } else { + None + } + }) + .map(|(index, field, field_serde_params)| { Param { field, + field_serde_params, container_attributes: FieldParamContainerAttributes { rename_all: rename_all.as_ref().and_then(|feature| { match feature { @@ -256,6 +265,8 @@ impl Parse for FieldFeatures { struct Param<'a> { /// Field in the container used to create a single parameter. field: &'a Field, + //// Field serde params parsed from field attributes. + field_serde_params: Option, /// Attributes on the container which are relevant for this macro. container_attributes: FieldParamContainerAttributes<'a>, /// Either serde rename all rule or into_params rename all rule if provided. @@ -329,6 +340,7 @@ impl Param<'_> { impl ToTokens for Param<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { let field = self.field; + let field_serde_params = &self.field_serde_params; let ident = &field.ident; let mut name = &*ident .as_ref() @@ -343,14 +355,12 @@ impl ToTokens for Param<'_> { name = &name[2..]; } - let field_param_serde = serde::parse_value(&field.attrs); - let (schema_features, mut param_features) = self.resolve_field_features(); let rename = param_features .pop_rename_feature() .map(|rename| rename.into_value()); - let rename_to = field_param_serde + let rename_to = field_serde_params .as_ref() .and_then(|field_param_serde| field_param_serde.rename.as_deref().map(Cow::Borrowed)) .or_else(|| rename.map(Cow::Owned)); @@ -406,7 +416,7 @@ impl ToTokens for Param<'_> { .unwrap_or(false); let non_required = (component.is_option() && !required) - || !component::is_required(field_param_serde.as_ref(), self.serde_container); + || !component::is_required(field_serde_params.as_ref(), self.serde_container); let required: Required = (!non_required).into(); tokens.extend(quote! { diff --git a/utoipa-gen/src/component/serde.rs b/utoipa-gen/src/component/serde.rs index 10af01b3..fb7f39ed 100644 --- a/utoipa-gen/src/component/serde.rs +++ b/utoipa-gen/src/component/serde.rs @@ -45,7 +45,11 @@ impl SerdeValue { let mut rest = *cursor; while let Some((tt, next)) = rest.token_tree() { match tt { - TokenTree::Ident(ident) if ident == "skip" || ident == "skip_serializing" => { + TokenTree::Ident(ident) + if ident == "skip" + || ident == "skip_serializing" + || ident == "skip_deserializing" => + { value.skip = true } TokenTree::Ident(ident) if ident == "skip_serializing_if" => { diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 516606c5..177d7eba 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -225,6 +225,7 @@ use self::{ /// * `rename = "..."` Supported **only** at the field or variant level. /// * `skip = "..."` Supported **only** at the field or variant level. /// * `skip_serializing = "..."` Supported **only** at the field or variant level. +/// * `skip_deserializing = "..."` Supported **only** at the field or variant level. /// * `skip_serializing_if = "..."` Supported **only** at the field level. /// * `with = ...` Supported **only at field level.** /// * `tag = "..."` Supported at the container level. `tag` attribute works as a [discriminator field][discriminator] for an enum. @@ -1747,6 +1748,9 @@ pub fn openapi(input: TokenStream) -> TokenStream { /// * `default` Supported at the container level and field level according to [serde attributes]. /// * `skip_serializing_if = "..."` Supported **only** at the field level. /// * `with = ...` Supported **only** at field level. +/// * `skip_serializing = "..."` Supported **only** at the field or variant level. +/// * `skip_deserializing = "..."` Supported **only** at the field or variant level. +/// * `skip = "..."` Supported **only** at the field level. /// /// Other _`serde`_ attributes will impact the serialization but will not be reflected on the generated OpenAPI doc. /// diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index c0ac5eca..4f1aa8eb 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use assert_json_diff::{assert_json_eq, assert_json_matches, CompareMode, Config, NumericMode}; use paste::paste; +use serde::Serialize; use serde_json::{json, Value}; use std::collections::HashMap; use utoipa::openapi::RefOr; @@ -1649,3 +1650,144 @@ fn derive_into_params_required() { ]) ) } + +#[test] +fn derive_into_params_with_serde_skip() { + #[derive(IntoParams, Serialize)] + #[into_params(parameter_in = Query)] + #[allow(unused)] + struct Params { + name: String, + name2: Option, + #[serde(skip)] + name3: Option, + } + + #[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": "name2", + "required": false, + "schema": { + "type": "string", + "nullable": true, + }, + }, + ]) + ) +} + +#[test] +fn derive_into_params_with_serde_skip_deserializing() { + #[derive(IntoParams, Serialize)] + #[into_params(parameter_in = Query)] + #[allow(unused)] + struct Params { + name: String, + name2: Option, + #[serde(skip_deserializing)] + name3: Option, + } + + #[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": "name2", + "required": false, + "schema": { + "type": "string", + "nullable": true, + }, + }, + ]) + ) +} + +#[test] +fn derive_into_params_with_serde_skip_serializing() { + #[derive(IntoParams, Serialize)] + #[into_params(parameter_in = Query)] + #[allow(unused)] + struct Params { + name: String, + name2: Option, + #[serde(skip_serializing)] + name3: Option, + } + + #[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": "name2", + "required": false, + "schema": { + "type": "string", + "nullable": true, + }, + }, + ]) + ) +}