Skip to main content

rustack_dynamodb_model/
attribute_value.rs

1//! DynamoDB `AttributeValue` type with custom serialization.
2//!
3//! `AttributeValue` is a tagged union where exactly one variant is present.
4//! The JSON wire format uses single-key objects like `{"S": "hello"}`.
5
6use std::{collections::HashMap, fmt};
7
8use serde::{
9    Deserialize, Deserializer, Serialize, Serializer,
10    de::{self, MapAccess, Visitor},
11    ser::SerializeMap,
12};
13
14/// DynamoDB attribute value.
15///
16/// Represented as a tagged union where exactly one variant is present.
17/// Numbers are always string-encoded to preserve arbitrary precision.
18#[derive(Debug, Clone, PartialEq)]
19pub enum AttributeValue {
20    /// String value.
21    S(String),
22    /// Number value (string-encoded for arbitrary precision).
23    N(String),
24    /// Binary value (base64-encoded in JSON).
25    B(bytes::Bytes),
26    /// String Set.
27    Ss(Vec<String>),
28    /// Number Set (string-encoded).
29    Ns(Vec<String>),
30    /// Binary Set (base64-encoded in JSON).
31    Bs(Vec<bytes::Bytes>),
32    /// Boolean value.
33    Bool(bool),
34    /// Null value.
35    Null(bool),
36    /// List of attribute values.
37    L(Vec<AttributeValue>),
38    /// Map of attribute values.
39    M(HashMap<String, AttributeValue>),
40}
41
42impl AttributeValue {
43    /// Returns `true` if this is a string value.
44    #[must_use]
45    pub fn is_s(&self) -> bool {
46        matches!(self, Self::S(_))
47    }
48
49    /// Returns `true` if this is a number value.
50    #[must_use]
51    pub fn is_n(&self) -> bool {
52        matches!(self, Self::N(_))
53    }
54
55    /// Returns `true` if this is a binary value.
56    #[must_use]
57    pub fn is_b(&self) -> bool {
58        matches!(self, Self::B(_))
59    }
60
61    /// Returns `true` if this is a boolean value.
62    #[must_use]
63    pub fn is_bool(&self) -> bool {
64        matches!(self, Self::Bool(_))
65    }
66
67    /// Returns `true` if this is a null value.
68    #[must_use]
69    pub fn is_null(&self) -> bool {
70        matches!(self, Self::Null(true))
71    }
72
73    /// Returns `true` if this is a list value.
74    #[must_use]
75    pub fn is_l(&self) -> bool {
76        matches!(self, Self::L(_))
77    }
78
79    /// Returns `true` if this is a map value.
80    #[must_use]
81    pub fn is_m(&self) -> bool {
82        matches!(self, Self::M(_))
83    }
84
85    /// Returns the string value if this is an `S` variant.
86    #[must_use]
87    pub fn as_s(&self) -> Option<&str> {
88        match self {
89            Self::S(s) => Some(s),
90            _ => None,
91        }
92    }
93
94    /// Returns the number string if this is an `N` variant.
95    #[must_use]
96    pub fn as_n(&self) -> Option<&str> {
97        match self {
98            Self::N(n) => Some(n),
99            _ => None,
100        }
101    }
102
103    /// Returns the map if this is an `M` variant.
104    #[must_use]
105    pub fn as_m(&self) -> Option<&HashMap<String, AttributeValue>> {
106        match self {
107            Self::M(m) => Some(m),
108            _ => None,
109        }
110    }
111
112    /// Returns the list if this is an `L` variant.
113    #[must_use]
114    pub fn as_l(&self) -> Option<&[AttributeValue]> {
115        match self {
116            Self::L(l) => Some(l),
117            _ => None,
118        }
119    }
120
121    /// Returns the boolean if this is a `Bool` variant.
122    #[must_use]
123    pub fn as_bool(&self) -> Option<bool> {
124        match self {
125            Self::Bool(b) => Some(*b),
126            _ => None,
127        }
128    }
129
130    /// Returns the DynamoDB type descriptor string (e.g., "S", "N", "BOOL").
131    #[must_use]
132    pub fn type_descriptor(&self) -> &'static str {
133        match self {
134            Self::S(_) => "S",
135            Self::N(_) => "N",
136            Self::B(_) => "B",
137            Self::Ss(_) => "SS",
138            Self::Ns(_) => "NS",
139            Self::Bs(_) => "BS",
140            Self::Bool(_) => "BOOL",
141            Self::Null(_) => "NULL",
142            Self::L(_) => "L",
143            Self::M(_) => "M",
144        }
145    }
146}
147
148impl Eq for AttributeValue {}
149
150impl std::hash::Hash for AttributeValue {
151    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
152        core::mem::discriminant(self).hash(state);
153        match self {
154            Self::S(s) => s.hash(state),
155            Self::N(n) => n.hash(state),
156            Self::B(b) => b.hash(state),
157            Self::Bool(b) | Self::Null(b) => b.hash(state),
158            Self::Ss(v) | Self::Ns(v) => v.hash(state),
159            Self::Bs(v) => {
160                for b in v {
161                    b.hash(state);
162                }
163            }
164            Self::L(v) => v.hash(state),
165            Self::M(m) => {
166                // Deterministic hash for maps: sort keys.
167                let mut pairs: Vec<_> = m.iter().collect();
168                pairs.sort_by_key(|(k, _)| *k);
169                for (k, v) in pairs {
170                    k.hash(state);
171                    v.hash(state);
172                }
173            }
174        }
175    }
176}
177
178impl fmt::Display for AttributeValue {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            Self::S(s) => write!(f, "{{S: {s}}}"),
182            Self::N(n) => write!(f, "{{N: {n}}}"),
183            Self::B(b) => write!(f, "{{B: {} bytes}}", b.len()),
184            Self::Ss(v) => write!(f, "{{SS: {v:?}}}"),
185            Self::Ns(v) => write!(f, "{{NS: {v:?}}}"),
186            Self::Bs(v) => write!(f, "{{BS: {} items}}", v.len()),
187            Self::Bool(b) => write!(f, "{{BOOL: {b}}}"),
188            Self::Null(b) => write!(f, "{{NULL: {b}}}"),
189            Self::L(v) => write!(f, "{{L: {} items}}", v.len()),
190            Self::M(m) => write!(f, "{{M: {} keys}}", m.len()),
191        }
192    }
193}
194
195impl Serialize for AttributeValue {
196    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
197        let mut map = serializer.serialize_map(Some(1))?;
198        match self {
199            Self::S(s) => map.serialize_entry("S", s)?,
200            Self::N(n) => map.serialize_entry("N", n)?,
201            Self::B(b) => {
202                use base64::Engine;
203                let encoded = base64::engine::general_purpose::STANDARD.encode(b);
204                map.serialize_entry("B", &encoded)?;
205            }
206            Self::Ss(v) => map.serialize_entry("SS", v)?,
207            Self::Ns(v) => map.serialize_entry("NS", v)?,
208            Self::Bs(v) => {
209                use base64::Engine;
210                let encoded: Vec<String> = v
211                    .iter()
212                    .map(|b| base64::engine::general_purpose::STANDARD.encode(b))
213                    .collect();
214                map.serialize_entry("BS", &encoded)?;
215            }
216            Self::Bool(b) => map.serialize_entry("BOOL", b)?,
217            Self::Null(b) => map.serialize_entry("NULL", b)?,
218            Self::L(list) => map.serialize_entry("L", list)?,
219            Self::M(m) => map.serialize_entry("M", m)?,
220        }
221        map.end()
222    }
223}
224
225impl<'de> Deserialize<'de> for AttributeValue {
226    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
227        deserializer.deserialize_map(AttributeValueVisitor)
228    }
229}
230
231struct AttributeValueVisitor;
232
233impl<'de> Visitor<'de> for AttributeValueVisitor {
234    type Value = AttributeValue;
235
236    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237        formatter.write_str("a DynamoDB AttributeValue object with exactly one type key")
238    }
239
240    fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<Self::Value, M::Error> {
241        let Some(key) = map.next_key::<String>()? else {
242            return Err(de::Error::custom(
243                "AttributeValue must have exactly one key",
244            ));
245        };
246
247        let value = match key.as_str() {
248            "S" => AttributeValue::S(map.next_value()?),
249            "N" => AttributeValue::N(map.next_value()?),
250            "B" => {
251                use base64::Engine;
252                let encoded: String = map.next_value()?;
253                let decoded = base64::engine::general_purpose::STANDARD
254                    .decode(&encoded)
255                    .map_err(de::Error::custom)?;
256                AttributeValue::B(bytes::Bytes::from(decoded))
257            }
258            "SS" => AttributeValue::Ss(map.next_value()?),
259            "NS" => AttributeValue::Ns(map.next_value()?),
260            "BS" => {
261                use base64::Engine;
262                let encoded: Vec<String> = map.next_value()?;
263                let decoded: Result<Vec<bytes::Bytes>, _> = encoded
264                    .iter()
265                    .map(|e| {
266                        base64::engine::general_purpose::STANDARD
267                            .decode(e)
268                            .map(bytes::Bytes::from)
269                    })
270                    .collect();
271                AttributeValue::Bs(decoded.map_err(de::Error::custom)?)
272            }
273            "BOOL" => AttributeValue::Bool(map.next_value()?),
274            "NULL" => AttributeValue::Null(map.next_value()?),
275            "L" => AttributeValue::L(map.next_value()?),
276            "M" => AttributeValue::M(map.next_value()?),
277            other => {
278                return Err(de::Error::unknown_field(
279                    other,
280                    &["S", "N", "B", "SS", "NS", "BS", "BOOL", "NULL", "L", "M"],
281                ));
282            }
283        };
284
285        Ok(value)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_should_serialize_string_value() {
295        let val = AttributeValue::S("hello".to_owned());
296        let json = serde_json::to_string(&val).unwrap();
297        assert_eq!(json, r#"{"S":"hello"}"#);
298    }
299
300    #[test]
301    fn test_should_serialize_number_value() {
302        let val = AttributeValue::N("42".to_owned());
303        let json = serde_json::to_string(&val).unwrap();
304        assert_eq!(json, r#"{"N":"42"}"#);
305    }
306
307    #[test]
308    fn test_should_serialize_bool_value() {
309        let val = AttributeValue::Bool(true);
310        let json = serde_json::to_string(&val).unwrap();
311        assert_eq!(json, r#"{"BOOL":true}"#);
312    }
313
314    #[test]
315    fn test_should_serialize_null_value() {
316        let val = AttributeValue::Null(true);
317        let json = serde_json::to_string(&val).unwrap();
318        assert_eq!(json, r#"{"NULL":true}"#);
319    }
320
321    #[test]
322    fn test_should_serialize_list_value() {
323        let val = AttributeValue::L(vec![
324            AttributeValue::S("a".to_owned()),
325            AttributeValue::N("1".to_owned()),
326        ]);
327        let json = serde_json::to_string(&val).unwrap();
328        assert_eq!(json, r#"{"L":[{"S":"a"},{"N":"1"}]}"#);
329    }
330
331    #[test]
332    fn test_should_roundtrip_map_value() {
333        let mut m = HashMap::new();
334        m.insert("key".to_owned(), AttributeValue::S("value".to_owned()));
335        let val = AttributeValue::M(m);
336        let json = serde_json::to_string(&val).unwrap();
337        let deserialized: AttributeValue = serde_json::from_str(&json).unwrap();
338        assert_eq!(val, deserialized);
339    }
340
341    #[test]
342    fn test_should_roundtrip_binary_value() {
343        let val = AttributeValue::B(bytes::Bytes::from_static(b"test data"));
344        let json = serde_json::to_string(&val).unwrap();
345        let deserialized: AttributeValue = serde_json::from_str(&json).unwrap();
346        assert_eq!(val, deserialized);
347    }
348
349    #[test]
350    fn test_should_deserialize_number_set() {
351        let json = r#"{"NS":["1","2","3"]}"#;
352        let val: AttributeValue = serde_json::from_str(json).unwrap();
353        assert!(matches!(val, AttributeValue::Ns(ref v) if v.len() == 3));
354    }
355
356    #[test]
357    fn test_should_deserialize_string_set() {
358        let json = r#"{"SS":["a","b"]}"#;
359        let val: AttributeValue = serde_json::from_str(json).unwrap();
360        assert!(matches!(val, AttributeValue::Ss(ref v) if v.len() == 2));
361    }
362}