Skip to main content

serde_tristate/
lib.rs

1use serde::{Deserialize, Deserializer, Serialize};
2#[cfg(feature = "macro")]
3pub use serde_tristate_macros::serde_tristate;
4
5/// Three-state value for HTTP PATCH request bodies.
6///
7/// - `Value(T)` — field present with a value
8/// - `None` — field present as JSON `null`
9/// - `Undefined` — field absent from the payload
10///
11/// # Serde integration
12///
13/// Annotate the containing struct/enum with `#[serde_tristate]` and derive
14/// `Serialize`/`Deserialize` normally — no per-field attributes needed.
15///
16/// ```ignore
17/// #[serde_tristate]
18/// #[derive(Serialize, Deserialize)]
19/// struct UpdateUser {
20///     name: Tristate<String>,
21///     age:  Tristate<u32>,
22/// }
23/// ```
24///
25#[derive(Default)]
26pub enum Tristate<T> {
27    Value(T),
28    None,
29    #[default]
30    Undefined,
31}
32
33impl<T> Tristate<T> {
34    pub fn is_undefined(&self) -> bool {
35        matches!(self, Tristate::Undefined)
36    }
37
38    pub fn is_none(&self) -> bool {
39        matches!(self, Tristate::None)
40    }
41
42    pub fn is_value(&self) -> bool {
43        matches!(self, Tristate::Value(_))
44    }
45}
46
47impl<T> From<T> for Tristate<T> {
48    fn from(v: T) -> Self {
49        Tristate::Value(v)
50    }
51}
52
53impl<T> From<Option<T>> for Tristate<T> {
54    fn from(opt: Option<T>) -> Self {
55        match opt {
56            Some(v) => Tristate::Value(v),
57            None => Tristate::None,
58        }
59    }
60}
61
62impl<T> From<Option<Option<T>>> for Tristate<T> {
63    fn from(opt: Option<Option<T>>) -> Self {
64        match opt {
65            None => Tristate::Undefined,
66            Some(None) => Tristate::None,
67            Some(Some(v)) => Tristate::Value(v),
68        }
69    }
70}
71
72impl<T> From<Tristate<T>> for Option<Option<T>> {
73    fn from(val: Tristate<T>) -> Self {
74        match val {
75            Tristate::Undefined => None,
76            Tristate::None => Some(None),
77            Tristate::Value(v) => Some(Some(v)),
78        }
79    }
80}
81
82impl<T> Tristate<T> {
83    /// Map `Value(v)` through `f`. `None` and `Undefined` pass through unchanged.
84    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Tristate<U> {
85        match self {
86            Tristate::Value(v) => Tristate::Value(f(v)),
87            Tristate::None => Tristate::None,
88            Tristate::Undefined => Tristate::Undefined,
89        }
90    }
91
92    /// Chain on `Value(v)`. `None` and `Undefined` pass through unchanged.
93    pub fn and_then<U, F: FnOnce(T) -> Tristate<U>>(self, f: F) -> Tristate<U> {
94        match self {
95            Tristate::Value(v) => f(v),
96            Tristate::None => Tristate::None,
97            Tristate::Undefined => Tristate::Undefined,
98        }
99    }
100
101    /// Return the contained value or `default` if `None` / `Undefined`.
102    pub fn unwrap_or(self, default: T) -> T {
103        match self {
104            Tristate::Value(v) => v,
105            _ => default,
106        }
107    }
108
109    /// Return the contained value or compute it from `f` if `None` / `Undefined`.
110    pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
111        match self {
112            Tristate::Value(v) => v,
113            _ => f(),
114        }
115    }
116}
117
118impl<T> Tristate<T> {
119    /// Apply to a required target field.
120    /// `Value` overwrites; `None` and `Undefined` are no-ops.
121    pub fn apply_to_tristate(self, target: &mut T) {
122        if let Tristate::Value(v) = self {
123            *target = v;
124        }
125    }
126
127    /// Apply to an optional target field.
128    /// `Value(v)` → `Some(v)`, `None` → `None`, `Undefined` → no-op.
129    pub fn apply_to_option(self, target: &mut Option<T>) {
130        match self {
131            Tristate::Value(v) => *target = Some(v),
132            Tristate::None => *target = None,
133            Tristate::Undefined => {}
134        }
135    }
136}
137
138impl<T: Serialize> Serialize for Tristate<T> {
139    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
140        match self {
141            Tristate::Value(v) => v.serialize(serializer),
142            Tristate::None => serializer.serialize_none(),
143            Tristate::Undefined => serializer.serialize_none(),
144        }
145    }
146}
147
148impl<'de, T: Deserialize<'de>> Deserialize<'de> for Tristate<T> {
149    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
150        Option::<T>::deserialize(deserializer).map(|opt| match opt {
151            Some(v) => Tristate::Value(v),
152            None => Tristate::None,
153        })
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use serde::{Deserialize, Serialize};
161
162    #[serde_tristate]
163    #[derive(Serialize, Deserialize)]
164    struct Dto {
165        name: Tristate<String>,
166        age: Tristate<u32>,
167    }
168
169    #[test]
170    fn serialize_value() {
171        let dto = Dto {
172            name: Tristate::Value("Alice".into()),
173            age: Tristate::Undefined,
174        };
175        assert_eq!(serde_json::to_string(&dto).unwrap(), r#"{"name":"Alice"}"#);
176    }
177
178    #[test]
179    fn serialize_none() {
180        let dto = Dto {
181            name: Tristate::None,
182            age: Tristate::Undefined,
183        };
184        assert_eq!(serde_json::to_string(&dto).unwrap(), r#"{"name":null}"#);
185    }
186
187    #[test]
188    fn serialize_undefined_skipped() {
189        let dto = Dto {
190            name: Tristate::Undefined,
191            age: Tristate::Undefined,
192        };
193        assert_eq!(serde_json::to_string(&dto).unwrap(), r#"{}"#);
194    }
195
196    #[test]
197    fn deserialize_value() {
198        let dto: Dto = serde_json::from_str(r#"{"name":"Bob","age":42}"#).unwrap();
199        assert!(matches!(dto.name, Tristate::Value(s) if s == "Bob"));
200        assert!(matches!(dto.age, Tristate::Value(42)));
201    }
202
203    #[test]
204    fn deserialize_null_as_none() {
205        let dto: Dto = serde_json::from_str(r#"{"name":null}"#).unwrap();
206        assert!(matches!(dto.name, Tristate::None));
207        assert!(matches!(dto.age, Tristate::Undefined));
208    }
209
210    #[test]
211    fn deserialize_absent_as_undefined() {
212        let dto: Dto = serde_json::from_str(r#"{}"#).unwrap();
213        assert!(matches!(dto.name, Tristate::Undefined));
214        assert!(matches!(dto.age, Tristate::Undefined));
215    }
216
217    #[serde_tristate]
218    #[derive(Serialize, Deserialize)]
219    #[serde(tag = "kind")]
220    enum Event {
221        Update {
222            name: Tristate<String>,
223            age: Tristate<u32>,
224        },
225        Delete,
226    }
227
228    #[test]
229    fn enum_serialize_value() {
230        let e = Event::Update {
231            name: Tristate::Value("Carol".into()),
232            age: Tristate::Undefined,
233        };
234        assert_eq!(
235            serde_json::to_string(&e).unwrap(),
236            r#"{"kind":"Update","name":"Carol"}"#
237        );
238    }
239
240    #[test]
241    fn enum_serialize_all_undefined() {
242        let e = Event::Update {
243            name: Tristate::Undefined,
244            age: Tristate::Undefined,
245        };
246        assert_eq!(serde_json::to_string(&e).unwrap(), r#"{"kind":"Update"}"#);
247    }
248
249    #[test]
250    fn enum_deserialize_value() {
251        let e: Event = serde_json::from_str(r#"{"kind":"Update","name":"Dave"}"#).unwrap();
252        assert!(
253            matches!(e, Event::Update { name: Tristate::Value(s), age: Tristate::Undefined } if s == "Dave")
254        );
255    }
256
257    #[test]
258    fn from_value() {
259        let p: Tristate<i32> = 42.into();
260        assert!(matches!(p, Tristate::Value(42)));
261    }
262
263    #[test]
264    fn from_some() {
265        let p: Tristate<i32> = Some(42).into();
266        assert!(matches!(p, Tristate::Value(42)));
267    }
268
269    #[test]
270    fn from_option_none() {
271        let p: Tristate<i32> = Option::<i32>::None.into();
272        assert!(matches!(p, Tristate::None));
273    }
274
275    #[test]
276    fn into_option_value() {
277        assert_eq!(
278            Into::<Option<Option<i32>>>::into(Tristate::Value(1)),
279            Some(Some(1))
280        );
281    }
282
283    #[test]
284    fn into_option_none() {
285        assert_eq!(
286            Into::<Option<Option<i32>>>::into(Tristate::<i32>::None),
287            Some(Option::None)
288        );
289    }
290
291    #[test]
292    fn into_option_undefined() {
293        assert_eq!(
294            Into::<Option<Option<i32>>>::into(Tristate::<i32>::Undefined),
295            Option::<Option<i32>>::None
296        );
297    }
298
299    #[test]
300    fn from_option_fn_some_some() {
301        assert!(matches!(Tristate::from(Some(Some(1))), Tristate::Value(1)));
302    }
303
304    #[test]
305    fn from_option_fn_some_none() {
306        assert!(matches!(
307            Tristate::<i32>::from(Some(Option::None)),
308            Tristate::None
309        ));
310    }
311
312    #[test]
313    fn from_option_fn_outer_none() {
314        assert!(matches!(
315            Tristate::<i32>::from(Option::<Option<i32>>::None),
316            Tristate::Undefined
317        ));
318    }
319
320    #[test]
321    fn from_option_roundtrip() {
322        let cases: [Tristate<i32>; 3] = [Tristate::Value(42), Tristate::None, Tristate::Undefined];
323        for t in cases {
324            let opt: Option<Option<i32>> = t.into();
325            assert!(matches!(Tristate::<i32>::from(opt), _));
326        }
327    }
328
329    #[test]
330    fn map_value() {
331        assert!(matches!(
332            Tristate::Value(2).map(|x| x * 3),
333            Tristate::Value(6)
334        ));
335    }
336
337    #[test]
338    fn map_none_passthrough() {
339        assert!(matches!(
340            Tristate::<i32>::None.map(|x| x * 3),
341            Tristate::None
342        ));
343    }
344
345    #[test]
346    fn and_then_value() {
347        let p = Tristate::Value(5).and_then(|x| {
348            if x > 3 {
349                Tristate::Value(x)
350            } else {
351                Tristate::None
352            }
353        });
354        assert!(matches!(p, Tristate::Value(5)));
355    }
356
357    #[test]
358    fn unwrap_or_value() {
359        assert_eq!(Tristate::Value(7).unwrap_or(0), 7);
360    }
361
362    #[test]
363    fn unwrap_or_undefined() {
364        assert_eq!(Tristate::<i32>::Undefined.unwrap_or(0), 0);
365    }
366
367    #[test]
368    fn apply_to_required_sets_value() {
369        let mut target = String::from("old");
370        Tristate::Value("new".to_string()).apply_to_tristate(&mut target);
371        assert_eq!(target, "new");
372    }
373
374    #[test]
375    fn apply_to_required_undefined_noop() {
376        let mut target = String::from("old");
377        Tristate::<String>::Undefined.apply_to_tristate(&mut target);
378        assert_eq!(target, "old");
379    }
380
381    #[test]
382    fn apply_to_option_sets_some() {
383        let mut target: Option<String> = Option::None;
384        Tristate::Value("hi".to_string()).apply_to_option(&mut target);
385        assert_eq!(target, Some("hi".to_string()));
386    }
387
388    #[test]
389    fn apply_to_option_none_clears() {
390        let mut target: Option<String> = Some("bye".to_string());
391        Tristate::<String>::None.apply_to_option(&mut target);
392        assert_eq!(target, Option::None);
393    }
394
395    #[test]
396    fn apply_to_option_undefined_noop() {
397        let mut target: Option<String> = Some("keep".to_string());
398        Tristate::<String>::Undefined.apply_to_option(&mut target);
399        assert_eq!(target, Some("keep".to_string()));
400    }
401}