dynamodb_expression/value/
scalar.rs

1use core::fmt::{self, LowerExp, UpperExp};
2
3use aws_sdk_dynamodb::{primitives::Blob, types::AttributeValue};
4
5use super::base64;
6use super::Num;
7
8/// <https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes>
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum Scalar {
11    /// DynamoDB [string](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-S)
12    /// value
13    String(String),
14    /// DynamoDB [numeric](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N)
15    /// value
16    Num(Num),
17    /// DynamoDB [boolean](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-BOOL)
18    /// value
19    Bool(bool),
20    /// DynamoDB [binary](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-B)
21    /// value
22    Binary(Vec<u8>),
23    /// DynamoDB [null](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-NULL)
24    /// value
25    Null,
26}
27
28impl Scalar {
29    /// Use when you need a [string][1] value for DynamoDB.
30    ///
31    /// See also: [`Value::new_string`]
32    ///
33    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-S
34    /// [`Value::new_string`]: crate::value::Value::new_string
35    pub fn new_string<T>(value: T) -> Self
36    where
37        T: Into<String>,
38    {
39        Self::String(value.into())
40    }
41
42    /// Use when you need a [numeric][1] value for DynamoDB.
43    ///
44    /// See also: [`Scalar::new_num_lower_exp`], [`Scalar::new_num_upper_exp`],
45    /// [`Value::new_num`], [`Num`]
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use dynamodb_expression::Scalar;
51    /// # use pretty_assertions::assert_eq;
52    ///
53    /// let value = Scalar::new_num(2600);
54    /// assert_eq!("2600", value.to_string());
55    ///
56    /// let value = Scalar::new_num(2600.0);
57    /// assert_eq!("2600", value.to_string());
58    /// ```
59    ///
60    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
61    /// [`Value::new_num`]: crate::value::Value::new_num
62    pub fn new_num<N>(value: N) -> Self
63    where
64        N: ToString + num::Num,
65    {
66        Self::Num(Num::new(value))
67    }
68
69    /// Use when you need a [numeric][1] value for DynamoDB in exponent form
70    /// (with a lowercase `e`).
71    ///
72    /// See also: [`Scalar::new_num`], [`Scalar::new_num_upper_exp`],
73    /// [`Value::new_num_lower_exp`], [`Num`]
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use dynamodb_expression::Scalar;
79    /// # use pretty_assertions::assert_eq;
80    ///
81    /// let value = Scalar::new_num_lower_exp(2600);
82    /// assert_eq!("2.6e3", value.to_string());
83    ///
84    /// let value = Scalar::new_num_lower_exp(2600.0);
85    /// assert_eq!("2.6e3", value.to_string());
86    /// ```
87    ///
88    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
89    /// [`Value::new_num_lower_exp`]: crate::value::Value::new_num_lower_exp
90    pub fn new_num_lower_exp<N>(value: N) -> Self
91    where
92        N: LowerExp + num::Num,
93    {
94        Self::Num(Num::new_lower_exp(value))
95    }
96
97    /// Use when you need a [numeric][1] value for DynamoDB in exponent form
98    /// (with an uppercase `e`).
99    ///
100    /// See also: [`Scalar::new_num`], [`Scalar::new_num_lower_exp`],
101    /// [`Value::new_num_upper_exp`], [`Num`]
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use dynamodb_expression::Scalar;
107    /// # use pretty_assertions::assert_eq;
108    ///
109    /// let value = Scalar::new_num_upper_exp(2600);
110    /// assert_eq!("2.6E3", value.to_string());
111    ///
112    /// let value = Scalar::new_num_upper_exp(2600.0);
113    /// assert_eq!("2.6E3", value.to_string());
114    /// ```
115    ///
116    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-N
117    /// [`Value::new_num_upper_exp`]: crate::value::Value::new_num_upper_exp
118    pub fn new_num_upper_exp<N>(value: N) -> Self
119    where
120        N: UpperExp + num::Num,
121    {
122        Self::Num(Num::new_upper_exp(value))
123    }
124
125    /// Use when you need a [boolean][1] value for DynamoDB.
126    ///
127    /// See also: [`Value::new_bool`]
128    ///
129    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-BOOL
130    /// [`Value::new_bool`]: crate::value::Value::new_bool
131    pub fn new_bool(b: bool) -> Self {
132        Self::Bool(b)
133    }
134
135    /// Use when you need a [binary][1] value for DynamoDB.
136    ///
137    /// See also: [`Value::new_binary`]
138    ///
139    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-B
140    /// [`Value::new_binary`]: crate::value::Value::new_binary
141    pub fn new_binary<B>(binary: B) -> Self
142    where
143        B: Into<Vec<u8>>,
144    {
145        Self::Binary(binary.into())
146    }
147
148    /// Use when you need a [null][1] value for DynamoDB.
149    ///
150    /// See also: [`Value::new_null`]
151    ///
152    /// [1]: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-NULL
153    /// [`Value::new_null`]: crate::value::Value::new_null
154    pub fn new_null() -> Self {
155        Self::Null
156    }
157
158    // Intentionally not using `impl From<Scalar> for AttributeValue` because
159    // I don't want to make this a public API people rely on. The purpose of this
160    // crate is not to make creating `AttributeValues` easier. They should try
161    // `serde_dynamo`.
162    pub(super) fn into_attribute_value(self) -> AttributeValue {
163        match self {
164            Scalar::String(s) => AttributeValue::S(s),
165            Scalar::Num(n) => n.into_attribute_value(),
166            Scalar::Bool(b) => AttributeValue::Bool(b),
167            Scalar::Binary(b) => AttributeValue::B(Blob::new(b)),
168            Scalar::Null => AttributeValue::Null(true),
169        }
170    }
171}
172
173impl fmt::Display for Scalar {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            Self::String(s) => serde_json::to_string(s).unwrap().fmt(f),
177            Self::Num(n) => n.fmt(f),
178            Self::Bool(b) => serde_json::Value::Bool(*b).to_string().fmt(f),
179            Self::Binary(b) => serde_json::Value::String(base64(b)).to_string().fmt(f),
180
181            // TODO: I'm pretty sure this isn't right.
182            // https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html#DDB-Type-AttributeValue-NULL
183            Self::Null => f.write_str("NULL"),
184        }
185    }
186}
187
188impl From<String> for Scalar {
189    fn from(value: String) -> Self {
190        Self::String(value)
191    }
192}
193
194impl From<&String> for Scalar {
195    fn from(value: &String) -> Self {
196        Self::String(value.to_owned())
197    }
198}
199
200impl From<&str> for Scalar {
201    fn from(value: &str) -> Self {
202        Self::String(value.to_owned())
203    }
204}
205
206impl From<&&str> for Scalar {
207    fn from(value: &&str) -> Self {
208        Self::String((*value).to_owned())
209    }
210}
211
212impl From<Num> for Scalar {
213    fn from(value: Num) -> Self {
214        Self::Num(value)
215    }
216}
217
218impl From<bool> for Scalar {
219    fn from(value: bool) -> Self {
220        Self::Bool(value)
221    }
222}
223
224impl From<Vec<u8>> for Scalar {
225    fn from(value: Vec<u8>) -> Self {
226        Self::Binary(value)
227    }
228}
229
230impl<const N: usize> From<[u8; N]> for Scalar {
231    fn from(value: [u8; N]) -> Self {
232        Self::Binary(value.into())
233    }
234}
235
236impl<const N: usize> From<&[u8; N]> for Scalar {
237    fn from(value: &[u8; N]) -> Self {
238        Self::Binary(value.into())
239    }
240}
241
242impl From<&[u8]> for Scalar {
243    fn from(value: &[u8]) -> Self {
244        Self::Binary(value.into())
245    }
246}
247
248impl From<()> for Scalar {
249    fn from(_: ()) -> Self {
250        Self::Null
251    }
252}
253
254impl FromIterator<u8> for Scalar {
255    fn from_iter<T>(iter: T) -> Self
256    where
257        T: IntoIterator<Item = u8>,
258    {
259        Self::Binary(iter.into_iter().collect())
260    }
261}
262
263#[cfg(test)]
264mod test {
265    use pretty_assertions::assert_eq;
266
267    use crate::Num;
268
269    use super::Scalar;
270
271    #[test]
272    fn string() {
273        let fish: &str = "fish";
274
275        let actual = Scalar::new_string(fish);
276        assert_eq!("\"fish\"", actual.to_string());
277
278        // &str
279        let actual = Scalar::from(fish);
280        assert_eq!("\"fish\"", actual.to_string());
281
282        // &&str
283        let phish: &&str = &fish;
284        let actual = Scalar::from(phish);
285        assert_eq!("\"fish\"", actual.to_string());
286
287        // &String
288        let phish: String = fish.into();
289        let actual = Scalar::from(&phish);
290        assert_eq!("\"fish\"", actual.to_string());
291
292        // String
293        let actual = Scalar::from(phish);
294        assert_eq!("\"fish\"", actual.to_string());
295    }
296
297    #[test]
298    fn numeric() {
299        let actual = Scalar::new_num(42);
300        assert_eq!("42", actual.to_string());
301
302        let actual = Scalar::from(Num::new(42));
303        assert_eq!("42", actual.to_string());
304    }
305
306    #[test]
307    fn boolean() {
308        assert_eq!("true", Scalar::new_bool(true).to_string());
309        assert_eq!("false", Scalar::new_bool(false).to_string());
310
311        assert_eq!("true", Scalar::from(true).to_string());
312        assert_eq!("false", Scalar::from(false).to_string());
313    }
314
315    #[test]
316    fn binary_vec() {
317        let bytes: Vec<u8> = b"fish".into();
318
319        let actual = Scalar::new_binary(bytes.clone());
320        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
321
322        let actual = Scalar::from(bytes);
323        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
324    }
325
326    #[test]
327    fn binary_array() {
328        let bytes: [u8; 4] = b"fish".to_owned();
329
330        let actual = Scalar::new_binary(bytes);
331        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
332
333        let actual = Scalar::from(bytes);
334        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
335    }
336
337    #[test]
338    fn binary_array_ref() {
339        let bytes: &[u8; 4] = b"fish";
340
341        #[allow(clippy::needless_borrows_for_generic_args)]
342        let actual = Scalar::new_binary(bytes);
343        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
344
345        let actual = Scalar::from(bytes);
346        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
347    }
348
349    #[test]
350    fn binary_slice() {
351        let bytes: &[u8] = &b"fish"[..];
352
353        let actual = Scalar::new_binary(bytes);
354        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
355
356        let actual = Scalar::from(bytes);
357        assert_eq!(r#""ZmlzaA==""#, actual.to_string());
358    }
359
360    #[test]
361    fn null() {
362        assert_eq!("NULL", Scalar::new_null().to_string());
363        assert_eq!("NULL", Scalar::from(()).to_string());
364    }
365}