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