I'm trying to implement support in a Rust API for a common JavaScript pattern, where updating an object will leave fields in the original object untouched if they are not provided in the update payload, but they will be cleared if they are provided as null
.
I think I'm pretty close, but the final sticking point is the custom deserialization logic to return types of my enum instead of the standard Option<T>
. This appears to be more complicated than implementing deserializers for more concrete types due to the need to deal with the absence of values. The current implementation of my new type (excluding the deserialization) looks like this:
/// Type alias for dealing with entry fields that are not provided separately to nulls.
/// Used for update behaviour- null erases fields, undefined leaves them untouched.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum MaybeUndefined<T> {
None,
Some(T),
Undefined,
}
// helper method for pulling values out to regular Option
// :TODO: see if this can be done without cloning the wrapped data for complex types like `Vec`
impl<T> MaybeUndefined<T> where T: Clone {
pub fn to_option(self) -> Option<T> {
match self {
MaybeUndefined::Some(val) => Option::Some(val.clone()),
_ => None,
}
}
}
impl<T> Into<Option<T>> for MaybeUndefined<T> where T: Clone {
fn into(self) -> Option<T> {
self.to_option()
}
}
// default to undefined, not null
// used by Serde to provide default values via `#[serde(default)]`
impl<T> Default for MaybeUndefined<T> {
fn default() -> MaybeUndefined<T> {
MaybeUndefined::Undefined
}
}
From here I have tried implementing traits for TryInto<MaybeUndefined<T>>
, but my code doesn't seem to be getting called so I don't think this is the correct approach.
FWIW the test case I'm trying to pass looks like this:
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
struct TestEntrySimple {
#[serde(default)]
test_field: MaybeUndefined<String>,
}
fn test_deserialization() {
let expected = TestEntrySimple { test_field: MaybeUndefined::Some("blah".to_string()) };
let input_json = "{\"test_field\":\"blah\"}";
assert_eq!(
Ok(expected),
TestEntrySimple::try_from(::serde_json::from_str(input_json)),
);
}
Any ideas what the correct approach to take here is?