Skip to content

Commit

Permalink
Adding serialize_with and deserialize_with attributes to struct f…
Browse files Browse the repository at this point in the history
…ields (poem-web#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
  • Loading branch information
SaculRennorb authored Feb 18, 2024
1 parent a7b9623 commit dbadd19
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 9 deletions.
34 changes: 25 additions & 9 deletions poem-openapi-derive/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ struct ObjectField {
skip_serializing_if_is_empty: bool,
#[darling(default)]
skip_serializing_if: Option<Path>,
#[darling(default)]
serialize_with: Option<Path>,
#[darling(default)]
deserialize_with: Option<Path>,
}

#[derive(FromDeriveInput)]
Expand Down Expand Up @@ -194,15 +198,22 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
};
});
}
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 {
Expand Down Expand Up @@ -239,9 +250,14 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
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);
}
}
Expand Down
58 changes: 58 additions & 0 deletions poem-openapi/tests/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<serde_json::Value> {
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<serde_json::Value>) -> poem_openapi::types::ParseResult<i32> {
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::<i32>();
let parse_b = b.trim().parse::<i32>();
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 }
);
}

0 comments on commit dbadd19

Please sign in to comment.