Skip to main content

paddle_rust_sdk/
nullable.rs

1use serde::Serialize;
2
3/// Three-state type for PATCH request fields.
4///
5/// The Paddle API uses PATCH semantics where omitting a field means "don't change",
6/// sending `null` means "clear the value", and sending a value means "set to this value".
7///
8/// - `Unchanged` - Field will be omitted from serialization (no change)
9/// - `Null` - Field will be serialized as `null` (clear the value)
10/// - `Value(T)` - Field will be serialized as the contained value
11#[derive(Clone, Debug, PartialEq)]
12pub enum Nullable<T> {
13    Unchanged,
14    Null,
15    Value(T),
16}
17
18impl<T> Nullable<T> {
19    pub fn is_unchanged(&self) -> bool {
20        matches!(self, Nullable::Unchanged)
21    }
22}
23
24impl<T> Default for Nullable<T> {
25    fn default() -> Self {
26        Nullable::Unchanged
27    }
28}
29
30impl<T> From<T> for Nullable<T> {
31    fn from(value: T) -> Self {
32        Nullable::Value(value)
33    }
34}
35
36impl From<&str> for Nullable<String> {
37    fn from(value: &str) -> Self {
38        Nullable::Value(value.to_string())
39    }
40}
41
42macro_rules! nullable_from_string_like {
43    ($($t:ty),* $(,)?) => {
44        $(
45            impl From<&str> for Nullable<$t> {
46                fn from(s: &str) -> Self {
47                    Nullable::Value(s.into())
48                }
49            }
50            impl From<String> for Nullable<$t> {
51                fn from(s: String) -> Self {
52                    Nullable::Value(s.into())
53                }
54            }
55        )*
56    };
57}
58
59use paddle_rust_sdk_types::ids::{AddressID, BusinessID, CustomerID, DiscountID};
60nullable_from_string_like!(CustomerID, AddressID, BusinessID, DiscountID);
61
62impl<T: Serialize> Serialize for Nullable<T> {
63    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
64    where
65        S: serde::Serializer,
66    {
67        match self {
68            Nullable::Unchanged => serializer.serialize_none(),
69            Nullable::Null => serializer.serialize_none(),
70            Nullable::Value(v) => v.serialize(serializer),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[derive(Serialize)]
80    struct TestStruct {
81        #[serde(skip_serializing_if = "Nullable::is_unchanged")]
82        field: Nullable<String>,
83    }
84
85    #[derive(Serialize)]
86    struct MultiField {
87        #[serde(skip_serializing_if = "Nullable::is_unchanged")]
88        a: Nullable<String>,
89        #[serde(skip_serializing_if = "Nullable::is_unchanged")]
90        b: Nullable<i32>,
91        #[serde(skip_serializing_if = "Nullable::is_unchanged")]
92        c: Nullable<bool>,
93    }
94
95    #[test]
96    fn unchanged_is_omitted() {
97        let s = TestStruct {
98            field: Nullable::Unchanged,
99        };
100        let json = serde_json::to_value(&s).unwrap();
101        assert_eq!(json, serde_json::json!({}));
102    }
103
104    #[test]
105    fn null_serializes_as_null() {
106        let s = TestStruct {
107            field: Nullable::Null,
108        };
109        let json = serde_json::to_value(&s).unwrap();
110        assert_eq!(json, serde_json::json!({"field": null}));
111    }
112
113    #[test]
114    fn value_serializes_as_value() {
115        let s = TestStruct {
116            field: Nullable::Value("hello".to_string()),
117        };
118        let json = serde_json::to_value(&s).unwrap();
119        assert_eq!(json, serde_json::json!({"field": "hello"}));
120    }
121
122    #[test]
123    fn mixed_fields() {
124        let s = MultiField {
125            a: Nullable::Unchanged,
126            b: Nullable::Null,
127            c: Nullable::Value(true),
128        };
129        let json = serde_json::to_value(&s).unwrap();
130        assert_eq!(json, serde_json::json!({"b": null, "c": true}));
131    }
132
133    #[test]
134    fn default_is_unchanged() {
135        let n: Nullable<String> = Nullable::default();
136        assert!(n.is_unchanged());
137    }
138
139    #[test]
140    fn from_value() {
141        let n: Nullable<String> = "hello".to_string().into();
142        assert_eq!(n, Nullable::Value("hello".to_string()));
143    }
144}