Skip to content

Commit

Permalink
Add support for serde skip in IntoParams derive (#743)
Browse files Browse the repository at this point in the history
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<String>,
     #[serde(skip)]
     name3: Option<String>,
 }
```
  • Loading branch information
juhaku committed Aug 20, 2023
1 parent c9c6913 commit 5fb25fa
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 7 deletions.
22 changes: 16 additions & 6 deletions utoipa-gen/src/component/into_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<SerdeValue>,
/// 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.
Expand Down Expand Up @@ -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()
Expand All @@ -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));
Expand Down Expand Up @@ -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! {
Expand Down
6 changes: 5 additions & 1 deletion utoipa-gen/src/component/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" => {
Expand Down
4 changes: 4 additions & 0 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand Down
142 changes: 142 additions & 0 deletions utoipa-gen/tests/path_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,
#[serde(skip)]
name3: Option<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": "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<String>,
#[serde(skip_deserializing)]
name3: Option<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": "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<String>,
#[serde(skip_serializing)]
name3: Option<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": "name2",
"required": false,
"schema": {
"type": "string",
"nullable": true,
},
},
])
)
}

0 comments on commit 5fb25fa

Please sign in to comment.