From dbadd19b61ef02986fbc999bf705796c9e3f7e67 Mon Sep 17 00:00:00 2001 From: SaculRennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Sun, 18 Feb 2024 14:38:20 +0100 Subject: [PATCH] Adding `serialize_with` and `deserialize_with` attributes to struct fields (#749) * added `serialize_with` and `deserialize_with` attribsto struct fields * clarified some details in comments * making the liter shut up * lore linter pleasing * hopefully the last time pleasing the linter --- poem-openapi-derive/src/object.rs | 34 +++++++++++++----- poem-openapi/tests/object.rs | 58 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs index f18a487057..d3ff889a7c 100644 --- a/poem-openapi-derive/src/object.rs +++ b/poem-openapi-derive/src/object.rs @@ -42,6 +42,10 @@ struct ObjectField { skip_serializing_if_is_empty: bool, #[darling(default)] skip_serializing_if: Option, + #[darling(default)] + serialize_with: Option, + #[darling(default)] + deserialize_with: Option, } #[derive(FromDeriveInput)] @@ -194,15 +198,22 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { }; }); } - None => deserialize_fields.push(quote! { - #[allow(non_snake_case)] - let #field_ident: #field_ty = { - let value = #crate_name::types::ParseFromJSON::parse_from_json(obj.remove(#field_name)) - .map_err(#crate_name::types::ParseError::propagate)?; - #validators_checker - value + None => { + let deserialize_function = match field.deserialize_with { + Some(ref function) => quote! { #function }, + None => quote! { #crate_name::types::ParseFromJSON::parse_from_json }, }; - }), + + deserialize_fields.push(quote! { + #[allow(non_snake_case)] + let #field_ident: #field_ty = { + let value = #deserialize_function(obj.remove(#field_name)) + .map_err(#crate_name::types::ParseError::propagate)?; + #validators_checker + value + }; + }) + } } } else { if args.deny_unknown_fields { @@ -239,9 +250,14 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { quote!(true) }; + let serialize_function = match field.serialize_with { + Some(ref function) => quote! { #function }, + None => quote! { #crate_name::types::ToJSON::to_json }, + }; + serialize_fields.push(quote! { if #check_is_none && #check_is_empty && #check_if { - if let ::std::option::Option::Some(value) = #crate_name::types::ToJSON::to_json(&self.#field_ident) { + if let ::std::option::Option::Some(value) = #serialize_function(&self.#field_ident) { object.insert(::std::string::ToString::to_string(#field_name), value); } } diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index e70aca4f32..1aabe737e0 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1019,3 +1019,61 @@ fn object_default_override_by_field() { } ); } + +// NOTE(Rennorb): The `serialize_with` and `deserialize_with` attributes don't add any additional validation, +// it's up to the library consumer to use them in ways were they don't violate the OpenAPI specification of the underlying type. +// +// In practice `serialize_with` only exists for the rounding case below, which could not be implemented in a different way before this +// (only by using a larger type), and `deserialize_with` just exists for parity. + +#[test] +fn serialize_with() { + #[derive(Debug, Object)] + struct Obj { + #[oai(serialize_with = "round")] + a: f32, + b: f32, + } + + // NOTE(Rennorb): Function signature in complice with `to_json` in the Type system. + // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now. + fn round(v: &f32) -> Option { + Some(serde_json::Value::from((*v as f64 * 1e5).round() / 1e5)) + } + + let obj = Obj { a: 0.3, b: 0.3 }; + + assert_eq!(obj.to_json(), Some(json!({"a": 0.3f64, "b": 0.3f32}))); +} + +#[test] +fn deserialize_with() { + #[derive(Debug, PartialEq, Object)] + struct Obj { + #[oai(deserialize_with = "add")] + a: i32, + } + + // NOTE(Rennorb): Function signature in complice with `parse_from_json` in the Type system. + // Would prefer the usual way of implementing this with a serializer reference, but this has to do for now. + fn add(value: Option) -> poem_openapi::types::ParseResult { + value + .as_ref() + .and_then(|v| v.as_str()) + .and_then(|s| s.split_once('+')) + .and_then(|(a, b)| { + let parse_a = a.trim().parse::(); + let parse_b = b.trim().parse::(); + match (parse_a, parse_b) { + (Ok(int_a), Ok(int_b)) => Some(int_a + int_b), + _ => None, + } + }) + .ok_or(poem_openapi::types::ParseError::custom("Unknown error")) // bad error, but its good enough for tests + } + + assert_eq!( + Obj::parse_from_json(Some(json!({"a": "3 + 4"}))).unwrap(), + Obj { a: 7 } + ); +}