1use serde::Deserialize;
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum NullablePatch<T> {
10 #[default]
12 Absent,
13 Null,
15 Value(T),
17}
18
19impl<T> NullablePatch<T> {
20 pub fn is_present(&self) -> bool {
22 !matches!(self, Self::Absent)
23 }
24}
25
26pub fn deserialize_nullable_patch_option<'de, D, T>(
35 deserializer: D,
36) -> std::result::Result<Option<NullablePatch<T>>, D::Error>
37where
38 D: serde::Deserializer<'de>,
39 T: Deserialize<'de>,
40{
41 Option::<T>::deserialize(deserializer).map(|value| Some(NullablePatch::from(value)))
42}
43
44impl<T> From<Option<T>> for NullablePatch<T> {
45 fn from(value: Option<T>) -> Self {
46 match value {
47 Some(value) => Self::Value(value),
48 None => Self::Null,
49 }
50 }
51}
52
53impl<'de, T> Deserialize<'de> for NullablePatch<T>
54where
55 T: Deserialize<'de>,
56{
57 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
58 where
59 D: serde::Deserializer<'de>,
60 {
61 Ok(match Option::<T>::deserialize(deserializer)? {
62 Some(value) => Self::Value(value),
63 None => Self::Null,
64 })
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71 use serde_json::json;
72
73 #[test]
74 fn patch_preserves_empty_values_and_rejects_wrong_types() {
75 let dto: PatchDto = serde_json::from_value(json!({"title":"", "description":""})).unwrap();
76 assert_eq!(dto.title, NullablePatch::Value(String::new()));
77 assert_eq!(dto.description, Some(NullablePatch::Value(String::new())));
78 for value in [
79 json!({"title":42}),
80 json!({"description":[]}),
81 json!({"title":false}),
82 ] {
83 assert!(serde_json::from_value::<PatchDto>(value).is_err());
84 }
85 }
86 #[derive(Debug, Deserialize, PartialEq, Eq)]
87 struct PatchDto {
88 #[serde(default)]
89 title: NullablePatch<String>,
90 #[serde(default, deserialize_with = "deserialize_nullable_patch_option")]
91 description: Option<NullablePatch<String>>,
92 }
93
94 #[test]
95 fn nullable_patch_deserializes_absent_null_and_value_fields() {
96 let dto: PatchDto = serde_json::from_value(json!({})).unwrap();
97 assert_eq!(dto.title, NullablePatch::Absent);
98 assert_eq!(dto.description, None);
99 assert!(!dto.title.is_present());
100
101 let dto: PatchDto =
102 serde_json::from_value(json!({ "title": null, "description": null })).unwrap();
103 assert_eq!(dto.title, NullablePatch::Null);
104 assert_eq!(dto.description, Some(NullablePatch::Null));
105 assert!(dto.title.is_present());
106
107 let dto: PatchDto = serde_json::from_value(json!({
108 "title": "new title",
109 "description": "new description"
110 }))
111 .unwrap();
112 assert_eq!(dto.title, NullablePatch::Value("new title".to_string()));
113 assert_eq!(
114 dto.description,
115 Some(NullablePatch::Value("new description".to_string()))
116 );
117 }
118}