Skip to main content

dynamodb_expression/value/
mod.rs

1//! Types related to values used in [DynamoDB update expressions][1]. For more, see [`Update`].
2//!
3//! [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html
4//! [`Update`]: crate::update::Update
5
6mod list;
7mod map;
8mod num;
9mod scalar;
10mod set;
11mod value_or_ref;
12
13pub use list::List;
14pub use map::Map;
15pub use num::Num;
16pub use scalar::Scalar;
17pub use set::{BinarySet, NumSet, Set, StringSet};
18pub use value_or_ref::{Ref, StringOrRef};
19
20pub(crate) use value_or_ref::ValueOrRef;
21
22use core::fmt::{self, LowerExp, UpperExp};
23use std::error::Error;
24
25use aws_sdk_dynamodb::{primitives::Blob, types::AttributeValue};
26use base64::{engine::general_purpose, Engine as _};
27use itertools::Itertools;
28
29/// A DynamoDB value
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub enum Value {
32    Scalar(Scalar),
33    Set(Set),
34    Map(Map),
35    List(List),
36}
37
38impl Value {
39    /// Use when you need a [string value][1] for DynamoDB.
40    ///
41    /// See also: [`Scalar::new_string`]
42    ///
43    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-S
44    pub fn new_string<T>(value: T) -> Self
45    where
46        T: Into<String>,
47    {
48        value.into().into()
49    }
50
51    /// Use when you need a [numeric value][1] for DynamoDB.
52    ///
53    /// See also: [`Num::new`], [`Scalar::new_num`],
54    /// [`Value::new_num_lower_exp`], [`Value::new_num_upper_exp`]
55    ///
56    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
57    pub fn new_num<N>(value: N) -> Self
58    where
59        N: ToString + ::num::Num,
60    {
61        Num::new(value).into()
62    }
63
64    /// Use when you need a [numeric value][1] for DynamoDB in exponent form
65    /// (with a lowercase `e`).
66    ///
67    /// See also: [`Num::new_lower_exp`], [`Scalar::new_num_lower_exp`],
68    /// [`Value::new_num_upper_exp`], [`Value::new_num`]
69    ///
70    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
71    pub fn new_num_lower_exp<N>(value: N) -> Self
72    where
73        N: LowerExp + ::num::Num,
74    {
75        Num::new_lower_exp(value).into()
76    }
77
78    /// Use when you need a [numeric value][1] for DynamoDB in exponent form
79    /// (with an uppercase `E`).
80    ///
81    /// See also: [`Num::new_upper_exp`], [`Scalar::new_num_upper_exp`],
82    /// [`Value::new_num_lower_exp`], [`Value::new_num`]
83    ///
84    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
85    pub fn new_num_upper_exp<N>(value: N) -> Self
86    where
87        N: UpperExp + ::num::Num,
88    {
89        Num::new_upper_exp(value).into()
90    }
91
92    /// Use when you need a [boolean value][1] for DynamoDB.
93    ///
94    /// See also: [`Scalar::new_bool`]
95    ///
96    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-BOOL
97    pub fn new_bool(b: bool) -> Self {
98        b.into()
99    }
100
101    /// Use when you need a [binary value][1] for DynamoDB.
102    ///
103    /// See also: [`Scalar::new_binary`]
104    ///
105    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-B
106    pub fn new_binary<B>(binary: B) -> Self
107    where
108        B: Into<Vec<u8>>,
109    {
110        binary.into().into()
111    }
112
113    /// Use when you need a [null value][1] for DynamoDB.
114    ///
115    /// See also: [`Scalar::new_null`]
116    ///
117    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-NULL
118    pub fn new_null() -> Self {
119        Scalar::new_null().into()
120    }
121
122    /// Creates a value to use as a [DynamoDB string set][1].
123    ///
124    /// See also: [`StringSet::new`], [`Set::new_string_set`]
125    ///
126    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
127    pub fn new_string_set<T>(string_set: T) -> Self
128    where
129        T: Into<StringSet>,
130    {
131        string_set.into().into()
132    }
133
134    /// Creates a value to use as a [DynamoDB number set][1].
135    ///
136    /// See also: [`NumSet::new`], [`Set::new_num_set`]
137    ///
138    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
139    pub fn new_num_set<T>(num_set: T) -> Self
140    where
141        T: Into<NumSet>,
142    {
143        num_set.into().into()
144    }
145
146    /// Creates a value to use as a [DynamoDB binary set][1].
147    ///
148    /// See also: [`BinarySet::new`], [`Set::new_binary_set`]
149    ///
150    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.SetTypes
151    pub fn new_binary_set<T>(binary_set: T) -> Self
152    where
153        T: Into<BinarySet>,
154    {
155        binary_set.into().into()
156    }
157
158    /// Creates a value to use as a [DynamoDB map][1].
159    ///
160    /// See also: [`Map::new`]
161    ///
162    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.Map
163    pub fn new_map<T>(map: T) -> Self
164    where
165        T: Into<Map>,
166    {
167        map.into().into()
168    }
169
170    /// Creates a value to use as a [DynamoDB list][1].
171    ///
172    /// See also: [`List::new`]
173    ///
174    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.List
175    pub fn new_list<T>(list: T) -> Self
176    where
177        T: Into<List>,
178    {
179        list.into().into()
180    }
181
182    // Intentionally not using `impl From<ScalarValue> for AttributeValue` because
183    // I don't want to make this a public API people rely on. The purpose of this
184    // crate is not to make creating `AttributeValues` easier. They should try
185    // `serde_dynamo`.
186    pub(crate) fn into_attribute_value(self) -> AttributeValue {
187        match self {
188            Self::Scalar(value) => value.into_attribute_value(),
189            Self::Set(value) => value.into_attribute_value(),
190            Self::Map(value) => value.into_attribute_value(),
191            Self::List(value) => value.into_attribute_value(),
192        }
193    }
194}
195
196impl From<Scalar> for Value {
197    fn from(value: Scalar) -> Self {
198        Self::Scalar(value)
199    }
200}
201
202impl From<String> for Value {
203    fn from(value: String) -> Self {
204        Scalar::from(value).into()
205    }
206}
207
208impl From<&String> for Value {
209    fn from(value: &String) -> Self {
210        Scalar::from(value).into()
211    }
212}
213
214impl From<&str> for Value {
215    fn from(value: &str) -> Self {
216        Scalar::from(value).into()
217    }
218}
219
220impl From<&&str> for Value {
221    fn from(value: &&str) -> Self {
222        Scalar::from(value).into()
223    }
224}
225
226impl From<Num> for Value {
227    fn from(value: Num) -> Self {
228        Scalar::from(value).into()
229    }
230}
231
232impl From<bool> for Value {
233    fn from(value: bool) -> Self {
234        Scalar::from(value).into()
235    }
236}
237
238impl From<Vec<u8>> for Value {
239    fn from(value: Vec<u8>) -> Self {
240        Scalar::from(value).into()
241    }
242}
243
244impl<const N: usize> From<[u8; N]> for Value {
245    fn from(value: [u8; N]) -> Self {
246        Scalar::from(value).into()
247    }
248}
249
250impl From<()> for Value {
251    fn from(value: ()) -> Self {
252        Scalar::from(value).into()
253    }
254}
255
256impl FromIterator<u8> for Value {
257    fn from_iter<T>(iter: T) -> Self
258    where
259        T: IntoIterator<Item = u8>,
260    {
261        Scalar::from_iter(iter).into()
262    }
263}
264
265impl From<Set> for Value {
266    fn from(set: Set) -> Self {
267        Self::Set(set)
268    }
269}
270
271impl From<StringSet> for Value {
272    fn from(string_set: StringSet) -> Self {
273        Self::Set(string_set.into())
274    }
275}
276
277impl From<NumSet> for Value {
278    fn from(num_set: NumSet) -> Self {
279        Self::Set(num_set.into())
280    }
281}
282
283impl From<BinarySet> for Value {
284    fn from(string_set: BinarySet) -> Self {
285        Self::Set(string_set.into())
286    }
287}
288
289impl From<Map> for Value {
290    fn from(map: Map) -> Self {
291        Self::Map(map)
292    }
293}
294
295impl From<List> for Value {
296    fn from(list: List) -> Self {
297        Self::List(list)
298    }
299}
300
301impl TryFrom<AttributeValue> for Value {
302    type Error = UnknownAttributeValueError;
303
304    /// This will only return an error if a new [`AttributeValue`] variant is
305    /// added to the AWS DynamoDB SDK and isn't supported here, yet.
306    ///
307    /// See: [`UnknownAttributeValueError`], [`AttributeValue::Unknown`]
308    fn try_from(value: AttributeValue) -> Result<Self, Self::Error> {
309        Ok(match value {
310            AttributeValue::B(b) => Scalar::Binary(b.into_inner()).into(),
311            AttributeValue::Bool(b) => Scalar::Bool(b).into(),
312            AttributeValue::Bs(bs) => {
313                BinarySet::from_iter(bs.into_iter().map(Blob::into_inner)).into()
314            }
315            AttributeValue::L(l) => List::from(
316                l.into_iter()
317                    .map(Self::try_from)
318                    .try_collect::<_, Vec<_>, _>()?,
319            )
320            .into(),
321            AttributeValue::M(m) => Map::from(
322                m.into_iter()
323                    .map(|(k, v)| Self::try_from(v).map(|v| (k, v)))
324                    .try_collect::<_, Vec<_>, _>()?,
325            )
326            .into(),
327            AttributeValue::N(n) => Num { n }.into(),
328            AttributeValue::Ns(ns) => NumSet::from_iter(ns.into_iter().map(|n| Num { n })).into(),
329            AttributeValue::Null(_null) => Scalar::Null.into(),
330            AttributeValue::S(s) => Scalar::String(s).into(),
331            AttributeValue::Ss(ss) => StringSet::from(ss).into(),
332            _ => return Err(UnknownAttributeValueError(value)),
333        })
334    }
335}
336
337impl fmt::Display for Value {
338    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339        match self {
340            Self::Scalar(value) => value.fmt(f),
341            Self::Set(value) => value.fmt(f),
342            Self::Map(value) => value.fmt(f),
343            Self::List(value) => value.fmt(f),
344        }
345    }
346}
347
348/// An error that may occur when converting an [`AttributeValue`] into a
349/// [`Value`] (via `.try_from()`/`.try_into()`). This will only occur if a new
350/// `AttributeValue` variant is added to the AWS DynamoDB SDK and isn't
351/// supported here, yet.
352///
353/// The [`AttributeValue`] with the unknown variant is included in this error.
354///
355/// See: [`AttributeValue::Unknown`]
356#[derive(Debug)]
357pub struct UnknownAttributeValueError(pub AttributeValue);
358
359impl fmt::Display for UnknownAttributeValueError {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        write!(f, "unknown AttributeValue variant: {:?}", self.0)
362    }
363}
364
365impl Error for UnknownAttributeValueError {}
366
367/// Produces base64 the way DynamoDB wants it.
368pub(crate) fn base64<T>(b: T) -> String
369where
370    T: AsRef<[u8]>,
371{
372    general_purpose::STANDARD.encode(b)
373}
374
375#[cfg(test)]
376mod test {
377    use aws_sdk_dynamodb::{primitives::Blob, types::AttributeValue};
378    use pretty_assertions::assert_eq;
379
380    use crate::value::{BinarySet, List, Map, Num, NumSet, StringSet};
381
382    use super::Value;
383
384    #[test]
385    fn display() {
386        assert_eq!(r#""a""#, Value::new_string("a").to_string());
387        assert_eq!(r#"1000"#, Value::new_num(1000).to_string());
388        assert_eq!(r#"1e3"#, Value::new_num_lower_exp(1000).to_string());
389        assert_eq!(r#"1E3"#, Value::new_num_upper_exp(1000).to_string());
390        assert_eq!(r#""YQ==""#, Value::new_binary("a").to_string());
391        assert_eq!("true", Value::new_bool(true).to_string());
392        assert_eq!("NULL", Value::new_null().to_string());
393
394        // Sets are unordered
395        assert_eq!(
396            r#"["a", "b", "c"]"#,
397            Value::new_string_set(["a", "c", "b"]).to_string()
398        );
399        assert_eq!(
400            r#"[-7, 1e3, 42]"#,
401            Value::new_num_set([Num::new_lower_exp(1000), Num::new(42), Num::new(-7)]).to_string()
402        );
403        assert_eq!(
404            r#"["YQ==", "Yg==", "Yw=="]"#,
405            Value::new_binary_set([b"a", b"b", b"c"]).to_string()
406        );
407
408        assert_eq!(
409            r#"[NULL, 8, "a string"]"#,
410            Value::new_list([
411                Value::new_null(),
412                Value::new_num(8),
413                Value::new_string("a string")
414            ])
415            .to_string()
416        );
417
418        assert_eq!(
419            r#"{n: 8, null: NULL, s: "a string"}"#,
420            Value::new_map([
421                (String::from("s"), Value::new_string("a string")),
422                (String::from("n"), Value::new_num(8)),
423                (String::from("null"), Value::new_null()),
424            ])
425            .to_string()
426        );
427    }
428
429    #[test]
430    fn from_attribute_value() {
431        assert_eq!(
432            Value::from(Map::from([
433                ("s", Value::from("a string")),
434                ("int", Value::from(Num::from(8))),
435                ("b", Value::from(b"foo".to_vec())),
436                ("null", Value::from(())),
437                ("yes", Value::from(true)),
438                ("no", Value::from(false)),
439                (
440                    "list",
441                    List::from([
442                        Value::from("foo"),
443                        Value::from(Num::from(42)),
444                        Value::from(()),
445                    ])
446                    .into(),
447                ),
448                ("ss", StringSet::from(["foo"]).into()),
449                ("ns", NumSet::from([Num::from(42)]).into()),
450                (
451                    "bs",
452                    BinarySet::from([b"foo".to_vec(), b"bar".to_vec(), b"baz".to_vec()]).into()
453                ),
454            ])),
455            Value::try_from(AttributeValue::M(
456                [
457                    ("s".to_string(), AttributeValue::S("a string".to_string())),
458                    ("int".to_string(), AttributeValue::N("8".to_string())),
459                    ("b".to_string(), AttributeValue::B(Blob::new(b"foo"))),
460                    ("null".to_string(), AttributeValue::Null(true)),
461                    ("yes".to_string(), AttributeValue::Bool(true)),
462                    ("no".to_string(), AttributeValue::Bool(false)),
463                    (
464                        "list".to_string(),
465                        AttributeValue::L(vec![
466                            AttributeValue::S("foo".to_string()),
467                            AttributeValue::N("42".to_string()),
468                            AttributeValue::Null(true),
469                        ]),
470                    ),
471                    (
472                        "ss".to_string(),
473                        AttributeValue::Ss(vec!["foo".to_string()])
474                    ),
475                    ("ns".to_string(), AttributeValue::Ns(vec!["42".to_string()])),
476                    (
477                        "bs".to_string(),
478                        AttributeValue::Bs(vec![
479                            Blob::new(b"foo"),
480                            Blob::new(b"bar"),
481                            Blob::new(b"baz"),
482                        ])
483                    ),
484                ]
485                .into_iter()
486                .collect(),
487            ))
488            .expect("Could not convert AttributeValue to Value"),
489        );
490    }
491}