Skip to main content

io_msgraph/v1/
types.rs

1//! Shared tri-state for the writable fields of Graph resources,
2//! distinguishing a field left out of a PATCH body from one explicitly
3//! cleared. See the update semantics at
4//! <https://learn.microsoft.com/en-us/graph/api/contact-update>.
5
6use core::ops::Deref;
7
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10/// A writable Microsoft Graph resource field: absent from the
11/// serialized body, explicitly null, or set to a value.
12///
13/// Graph PATCH semantics make the three states meaningful: an omitted
14/// field keeps its stored value, a null one is cleared, a present one
15/// is replaced. Collections clear through either `Null` or `Set` of an
16/// empty collection (an explicit `[]` on the wire). Deserialization
17/// maps a JSON null onto `Null` and a missing field onto `Unset` (via
18/// the serde `default`).
19#[derive(Debug, Clone, Default, Eq, PartialEq)]
20pub enum MsgraphField<T> {
21    /// Left out of the serialized body; an update preserves the stored
22    /// value.
23    #[default]
24    Unset,
25    /// Serialized as an explicit null; an update clears the stored
26    /// value.
27    Null,
28    /// Serialized as the value itself.
29    Set(T),
30}
31
32impl<T> MsgraphField<T> {
33    /// True for the variant skipped by serialization.
34    pub fn is_unset(&self) -> bool {
35        matches!(self, Self::Unset)
36    }
37
38    /// The set value, `None` for both unset and null.
39    pub fn as_option(&self) -> Option<&T> {
40        match self {
41            Self::Set(value) => Some(value),
42            _ => None,
43        }
44    }
45
46    /// The set value by value, `None` for both unset and null.
47    pub fn into_option(self) -> Option<T> {
48        match self {
49            Self::Set(value) => Some(value),
50            _ => None,
51        }
52    }
53
54    /// `Set` for `Some`, `Null` for `None`.
55    ///
56    /// This is the natural encoding of a full-state update body, where
57    /// every managed field is either replaced or cleared.
58    pub fn set_or_null(value: Option<T>) -> Self {
59        match value {
60            Some(value) => Self::Set(value),
61            None => Self::Null,
62        }
63    }
64}
65
66impl<T: Deref> MsgraphField<T> {
67    /// The set value dereferenced (e.g. `&str` out of a `String`
68    /// field), `None` for both unset and null.
69    pub fn as_deref(&self) -> Option<&T::Target> {
70        self.as_option().map(|value| value.deref())
71    }
72}
73
74impl<T> From<T> for MsgraphField<T> {
75    fn from(value: T) -> Self {
76        Self::Set(value)
77    }
78}
79
80impl<T: Serialize> Serialize for MsgraphField<T> {
81    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
82        match self {
83            // NOTE: Unset only reaches here without the paired
84            // skip_serializing_if attribute; null is the least wrong
85            // encoding then.
86            Self::Unset | Self::Null => serializer.serialize_none(),
87            Self::Set(value) => value.serialize(serializer),
88        }
89    }
90}
91
92impl<'de, T: Deserialize<'de>> Deserialize<'de> for MsgraphField<T> {
93    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
94        match Option::<T>::deserialize(deserializer)? {
95            Some(value) => Ok(Self::Set(value)),
96            None => Ok(Self::Null),
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use alloc::{string::String, vec, vec::Vec};
104
105    use serde::{Deserialize, Serialize};
106    use serde_json::json;
107
108    use crate::v1::MsgraphField;
109
110    #[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
111    struct Body {
112        #[serde(default, skip_serializing_if = "MsgraphField::is_unset")]
113        name: MsgraphField<String>,
114        #[serde(default, skip_serializing_if = "MsgraphField::is_unset")]
115        phones: MsgraphField<Vec<String>>,
116    }
117
118    #[test]
119    fn unset_fields_are_omitted() {
120        let body = serde_json::to_value(Body::default()).unwrap();
121        assert_eq!(body, json!({}));
122    }
123
124    #[test]
125    fn null_fields_serialize_as_explicit_null() {
126        let body = Body {
127            name: MsgraphField::Null,
128            phones: MsgraphField::Null,
129        };
130        assert_eq!(
131            serde_json::to_value(&body).unwrap(),
132            json!({ "name": null, "phones": null })
133        );
134    }
135
136    #[test]
137    fn set_fields_serialize_as_values_including_empty_collections() {
138        let body = Body {
139            name: MsgraphField::Set(String::from("Jane")),
140            phones: MsgraphField::Set(vec![]),
141        };
142        assert_eq!(
143            serde_json::to_value(&body).unwrap(),
144            json!({ "name": "Jane", "phones": [] })
145        );
146    }
147
148    #[test]
149    fn deserialization_distinguishes_missing_from_null() {
150        let body: Body = serde_json::from_value(json!({ "name": null })).unwrap();
151        assert_eq!(body.name, MsgraphField::Null);
152        assert_eq!(body.phones, MsgraphField::Unset);
153
154        let body: Body = serde_json::from_value(json!({ "phones": ["+33"] })).unwrap();
155        assert_eq!(body.name, MsgraphField::Unset);
156        assert_eq!(body.phones, MsgraphField::Set(vec![String::from("+33")]));
157    }
158}