From a83369cfadcd731e8af6ada18a65e0ddffc4f5be Mon Sep 17 00:00:00 2001 From: Rennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:39:21 +0100 Subject: [PATCH 1/5] added `serialize_with` and `deserialize_with` attribsto struct fields --- poem-openapi-derive/src/object.rs | 34 ++++++++++++++------ poem-openapi/tests/object.rs | 52 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs index f18a487057..e6e5cbf617 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..7a79b5f13d 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1019,3 +1019,55 @@ 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 above, which could not be implemented in a different way before this, +// and `deserialize_with` only 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 }); +} + From 234dbad655d9885bb613a16ff948aa77eb7aea94 Mon Sep 17 00:00:00 2001 From: Rennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:41:57 +0100 Subject: [PATCH 2/5] clarified some details in comments --- poem-openapi/tests/object.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index 7a79b5f13d..a8184353aa 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1023,8 +1023,8 @@ 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 above, which could not be implemented in a different way before this, -// and `deserialize_with` only exists for parity. +// 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() { From ad623c5f57b7214353fa277e56f2de6aa60511e5 Mon Sep 17 00:00:00 2001 From: Rennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:56:41 +0100 Subject: [PATCH 3/5] making the liter shut up --- poem-openapi-derive/src/object.rs | 6 +++--- poem-openapi/tests/object.rs | 34 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs index e6e5cbf617..57bbfaf289 100644 --- a/poem-openapi-derive/src/object.rs +++ b/poem-openapi-derive/src/object.rs @@ -201,7 +201,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { None => { let deserialize_function = match field.deserialize_with { Some(ref function) => quote! { #function }, - None => quote!{ #crate_name::types::ParseFromJSON::parse_from_json } + None => quote! { #crate_name::types::ParseFromJSON::parse_from_json } }; deserialize_fields.push(quote! { @@ -251,8 +251,8 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { }; let serialize_function = match field.serialize_with { - Some(ref function) => quote!{ #function }, - None => quote!{ #crate_name::types::ToJSON::to_json }, + Some(ref function) => quote! { #function }, + None => quote! { #crate_name::types::ToJSON::to_json }, }; serialize_fields.push(quote! { diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index a8184353aa..61f7cedcac 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1020,7 +1020,7 @@ fn object_default_override_by_field() { ); } -//NOTE(Rennorb): The `serialize_with` and `deserialize_with` attributes don't add any additional validation, +// 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 @@ -1035,9 +1035,9 @@ fn serialize_with() { b: f32, } - //NOTE(Rennorb): Function signature in complice with `to_json` in the Type system. + // 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 { + fn round(v: &f32) -> Option { Some(serde_json::Value::from((*v as f64 * 1e5).round() / 1e5)) } @@ -1054,20 +1054,28 @@ fn deserialize_with() { a: i32, } - //NOTE(Rennorb): Function signature in complice with `parse_from_json` in the Type system. + // 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 + 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 }); + assert_eq!( + Obj::parse_from_json(Some(json!({"a": "3 + 4"}))).unwrap(), + Obj { a: 7 } + ); } From b7feded50bd87c1ff9f44cc2fb697248e7b9f6a0 Mon Sep 17 00:00:00 2001 From: Rennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Thu, 15 Feb 2024 17:59:51 +0100 Subject: [PATCH 4/5] lore linter pleasing --- poem-openapi-derive/src/object.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs index 57bbfaf289..f377cc7f2d 100644 --- a/poem-openapi-derive/src/object.rs +++ b/poem-openapi-derive/src/object.rs @@ -201,7 +201,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { None => { let deserialize_function = match field.deserialize_with { Some(ref function) => quote! { #function }, - None => quote! { #crate_name::types::ParseFromJSON::parse_from_json } + None => quote! { #crate_name::types::ParseFromJSON::parse_from_json }, }; deserialize_fields.push(quote! { @@ -213,7 +213,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { value }; }) - }, + } } } else { if args.deny_unknown_fields { From 3bb789e29d3582b3edb57ccf505a9975a0e6854a Mon Sep 17 00:00:00 2001 From: Rennorb <18741506+SaculRennorb@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:22:56 +0100 Subject: [PATCH 5/5] hopefully the last time pleasing the linter --- poem-openapi-derive/src/object.rs | 2 +- poem-openapi/tests/object.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/poem-openapi-derive/src/object.rs b/poem-openapi-derive/src/object.rs index f377cc7f2d..d3ff889a7c 100644 --- a/poem-openapi-derive/src/object.rs +++ b/poem-openapi-derive/src/object.rs @@ -203,7 +203,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult { 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 = { diff --git a/poem-openapi/tests/object.rs b/poem-openapi/tests/object.rs index 61f7cedcac..1aabe737e0 100644 --- a/poem-openapi/tests/object.rs +++ b/poem-openapi/tests/object.rs @@ -1072,10 +1072,8 @@ fn deserialize_with() { .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 } ); } -