momento_functions_host/aws/
ddb.rs

1//! Host interfaces for working with AWS DynamoDB
2//!
3//! It is recommended that you write type bindings for the types in your tables.
4//! See the examples on [Item] for how to do this.
5
6use super::auth;
7use base64::Engine;
8use momento_functions_wit::host::momento::host;
9use momento_functions_wit::host::momento::host::aws_ddb::DdbError;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Dynamodb client for host interfaces.
14///
15/// This client uses Momento's host-provided AWS communication channel, which
16/// is kept hot at all times. When your Function has not run in several days or more,
17/// the channel is still hot and ready, keeping your Function invocations predictable
18/// even when your demand is unpredictable.
19pub struct DynamoDBClient {
20    client: host::aws_ddb::Client,
21}
22
23/// An error returned from a Dynamo call.
24#[derive(Debug, thiserror::Error)]
25pub enum DynamoDBError {
26    /// When calling Dynamo, Items are serialized/deserialized to/from JSON.
27    /// This error indicates that a failure occurred when doing so.
28    #[error("Failed to serialize/deserialize host json: {cause}")]
29    SerDeJson {
30        /// The underlying (de)serialization error.
31        #[from]
32        cause: serde_json::error::Error,
33    },
34    /// An error from the Dynamo host interface.
35    #[error(transparent)]
36    Dynamo(#[from] DdbError),
37}
38
39/// An error occurred while using the extracting get_item wrapper.
40#[derive(Debug, thiserror::Error)]
41pub enum GetItemError<E> {
42    /// An error occurred when calling the provided TryFrom implementation.
43    TryFrom {
44        /// The underlying error.
45        cause: E,
46    },
47    /// An error occurred when calling Dynamo.
48    Dynamo {
49        /// The underlying error.
50        #[from]
51        cause: DynamoDBError,
52    },
53}
54
55impl DynamoDBClient {
56    /// Create a new DynamoDB client.
57    ///
58    /// ```rust
59    /// # use momento_functions_host::aws::auth::AwsCredentialsProvider;
60    /// # use momento_functions_host::aws::ddb::DynamoDBClient;
61    /// # use momento_functions_host::build_environment_aws_credentials;
62    /// # use momento_functions_wit::host::momento::host::aws_auth::AuthError;
63    /// # fn f() -> Result<(), AuthError> {
64    /// let client = DynamoDBClient::new(
65    ///     &AwsCredentialsProvider::new(
66    ///         "us-east-1",
67    ///         build_environment_aws_credentials!()
68    ///     )?
69    /// );
70    /// # Ok(())
71    /// # }
72    /// ```
73    pub fn new(credentials: &auth::AwsCredentialsProvider) -> Self {
74        Self {
75            client: host::aws_ddb::Client::new(credentials.resource()),
76        }
77    }
78
79    /// Get an item from a DynamoDB table.
80    ///
81    /// Examples:
82    /// ________
83    /// Custom bound types:
84    /// ```rust
85    /// use momento_functions_host::aws::ddb::{AttributeValue, DynamoDBClient, DynamoDBError, GetItemError, Item};
86    ///
87    /// /// Look up an item from a DynamoDB table and deserialize it into a MyStruct.
88    /// /// Returns None if the item does not exist.
89    /// fn get_my_struct(client: &DynamoDBClient, which_one: &str) -> Result<Option<MyStruct>, GetItemError<String>> {
90    ///     client.get_item("my_table", ("some_attribute", which_one))
91    /// }
92    ///
93    /// struct MyStruct {
94    ///     some_attribute: String,
95    /// }
96    ///
97    /// // Boilerplate to convert from dynamodb format
98    ///
99    /// impl TryFrom<Item> for MyStruct {
100    ///     type Error = String;
101    ///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
102    ///         Ok(Self {
103    ///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
104    ///         })
105    ///     }
106    /// }
107    pub fn get_item<V, E>(
108        &self,
109        table_name: impl Into<String>,
110        key: impl Into<Key>,
111    ) -> Result<Option<V>, GetItemError<E>>
112    where
113        V: TryFrom<Item, Error = E>,
114    {
115        match self.get_item_raw(table_name, key)? {
116            Some(item) => Ok(Some(
117                V::try_from(item).map_err(|e| GetItemError::TryFrom { cause: e })?,
118            )),
119            None => Ok(None),
120        }
121    }
122
123    /// Get an item from a DynamoDB table.
124    ///
125    /// Examples:
126    /// ________
127    /// ```rust
128    /// use momento_functions_host::aws::ddb::{DynamoDBClient, DynamoDBError, Item};
129    ///
130    /// /// Read an item from a DynamoDB table "my_table" with a S key attribute "some_attribute".
131    /// fn get_some_item(client: &DynamoDBClient, which_one: &str) -> Result<Option<Item>, DynamoDBError> {
132    ///     client.get_item_raw("my_table", ("some_attribute", which_one))
133    /// }
134    /// ```
135    pub fn get_item_raw(
136        &self,
137        table_name: impl Into<String>,
138        key: impl Into<Key>,
139    ) -> Result<Option<Item>, DynamoDBError> {
140        let key: Key = key.into();
141
142        let output = self.client.get_item(&host::aws_ddb::GetItemRequest {
143            table_name: table_name.into(),
144            key: key.into(),
145            consistent_read: false,
146            return_consumed_capacity: host::aws_ddb::ReturnConsumedCapacity::None,
147            projection_expression: None,
148            expression_attribute_names: None,
149        })?;
150
151        match output.item {
152            Some(item) => {
153                match item {
154                    // {
155                    //   "profile_picture": { "B": "base64 string" },
156                    //   "is_valid": { "BOOL": true },
157                    //   "pictures": { "BS": ["base64 1", "base64 2"] },
158                    //   "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
159                    //   "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
160                    //   "age": { "N": "23" },
161                    //   "favorite_birthdays": { "NS": ["17", "25"] },
162                    //   "children": { "NULL": true },
163                    //   "name": { "S": "arthur" },
164                    //   "friends": { "SS": ["bob", "alice"] }
165                    // }
166                    host::aws_ddb::Item::Json(j) => Ok(serde_json::from_str(&j)?),
167                }
168            }
169            None => Ok(None),
170        }
171    }
172
173    /// Put an item into a DynamoDB table.
174    ///
175    /// Examples:
176    /// Raw item:
177    /// ________
178    /// ```rust
179    /// # use momento_functions_host::aws::ddb::{DynamoDBClient, DynamoDBError};
180    ///
181    /// # fn put_some_item(client: &DynamoDBClient) -> Result<(), DynamoDBError> {
182    /// client.put_item(
183    ///     "my_table",
184    ///     [
185    ///         ("some_attribute", "some S value"),
186    ///         ("some_other_attribute", "some other S value"),
187    ///     ]
188    /// )
189    /// # }
190    /// ```
191    /// ________
192    /// Custom bound types:
193    /// ```rust
194    /// use momento_functions_host::aws::ddb::{AttributeValue, DynamoDBClient, DynamoDBError, Item};
195    ///
196    /// /// Store an item in a DynamoDB table by serializing a MyStruct.
197    /// fn put_my_struct(client: &DynamoDBClient, which_one: MyStruct) -> Result<(), DynamoDBError> {
198    ///     client.put_item("my_table", which_one)
199    /// }
200    ///
201    /// struct MyStruct {
202    ///     some_attribute: String,
203    /// }
204    ///
205    /// // Boilerplate to convert into dynamodb format
206    /// impl From<MyStruct> for Item {
207    ///     fn from(value: MyStruct) -> Self {
208    ///         [
209    ///             ("some_attribute", AttributeValue::from(value.some_attribute)),
210    ///         ].into()
211    ///     }
212    /// }
213    /// ```
214    pub fn put_item(
215        &self,
216        table_name: impl Into<String>,
217        item: impl Into<Item>,
218    ) -> Result<(), DynamoDBError> {
219        let item: Item = item.into();
220
221        let _output = self.client.put_item(&host::aws_ddb::PutItemRequest {
222            table_name: table_name.into(),
223            item: host::aws_ddb::Item::Json(serde_json::to_string(&item)?),
224            condition: None,
225            return_values: host::aws_ddb::ReturnValues::None,
226            return_consumed_capacity: host::aws_ddb::ReturnConsumedCapacity::None,
227        })?;
228
229        Ok(())
230    }
231}
232
233/// DynamoDB key type
234pub enum Key {
235    /// Hash key only
236    Hash {
237        /// Hash key name
238        key: String,
239        /// Hash key value
240        value: KeyValue,
241    },
242    /// Hash and range key
243    HashRange {
244        /// Hash key name
245        hash_key: String,
246        /// Hash key value
247        hash_value: KeyValue,
248        /// Range key name
249        range_key: String,
250        /// Range key value
251        range_value: KeyValue,
252    },
253}
254
255/// DynamoDB value type for keys
256#[derive(Debug, serde::Serialize, serde::Deserialize)]
257pub enum KeyValue {
258    /// S value
259    #[serde(rename = "S")]
260    String(String),
261    /// N value
262    #[serde(rename = "N")]
263    Number(i64),
264    /// B value
265    #[serde(rename = "B")]
266    Binary(Vec<u8>),
267}
268
269impl<K, V> From<(K, V)> for Key
270where
271    K: Into<String>,
272    V: Into<KeyValue>,
273{
274    fn from((k, v): (K, V)) -> Self {
275        Key::Hash {
276            key: k.into(),
277            value: v.into(),
278        }
279    }
280}
281
282impl From<Key> for Vec<host::aws_ddb::KeyAttribute> {
283    fn from(value: Key) -> Self {
284        match value {
285            Key::Hash { key, value } => vec![host::aws_ddb::KeyAttribute {
286                name: key,
287                value: value.into(),
288            }],
289            Key::HashRange {
290                hash_key,
291                hash_value,
292                range_key,
293                range_value,
294            } => vec![
295                host::aws_ddb::KeyAttribute {
296                    name: hash_key,
297                    value: hash_value.into(),
298                },
299                host::aws_ddb::KeyAttribute {
300                    name: range_key,
301                    value: range_value.into(),
302                },
303            ],
304        }
305    }
306}
307
308impl From<String> for KeyValue {
309    fn from(value: String) -> Self {
310        KeyValue::String(value)
311    }
312}
313impl From<&str> for KeyValue {
314    fn from(value: &str) -> Self {
315        KeyValue::String(value.to_string())
316    }
317}
318impl From<i64> for KeyValue {
319    fn from(value: i64) -> Self {
320        KeyValue::Number(value)
321    }
322}
323impl From<Vec<u8>> for KeyValue {
324    fn from(value: Vec<u8>) -> Self {
325        KeyValue::Binary(value)
326    }
327}
328impl From<&[u8]> for KeyValue {
329    fn from(value: &[u8]) -> Self {
330        KeyValue::Binary(value.to_vec())
331    }
332}
333impl From<KeyValue> for host::aws_ddb::KeyValue {
334    fn from(value: KeyValue) -> Self {
335        match value {
336            KeyValue::String(s) => host::aws_ddb::KeyValue::S(s),
337            KeyValue::Number(n) => host::aws_ddb::KeyValue::N(n.to_string()),
338            KeyValue::Binary(b) => host::aws_ddb::KeyValue::B(
339                base64::engine::general_purpose::STANDARD_NO_PAD.encode(b),
340            ),
341        }
342    }
343}
344
345/// dynamodb-formatted json looks something like this:
346/// ```json
347/// {
348///   "profile_picture": { "B": "base64 string" },
349///   "is_valid": { "BOOL": true },
350///   "pictures": { "BS": ["base64 1", "base64 2"] },
351///   "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
352///   "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
353///   "age": { "N": "23" },
354///   "favorite_birthdays": { "NS": ["17", "25"] },
355///   "children": { "NULL": true },
356///   "name": { "S": "arthur" },
357///   "friends": { "SS": ["bob", "alice"] }
358/// }
359/// ```
360/// This stuff exists mostly because WIT maintainers consider list<t> to be dependent on t,
361/// which causes much consternation with regard to serialization. Eventually they will
362/// likely work it out like json, protocol buffers, msgpack, and many other serialization
363/// formats before it.
364///
365/// Examples:
366/// ________
367/// Basic explicit lists:
368/// ```rust
369/// use momento_functions_host::aws::ddb::Item;
370/// let item: Item = vec![("some key", "some value")].into();
371/// let item: Item = vec![("some key", 42)].into();
372/// ```
373/// ________
374/// Custom bound types:
375/// ```rust
376/// use momento_functions_host::aws::ddb::{AttributeValue, Item};
377/// struct MyStruct {
378///     some_attribute: String,
379/// }
380///
381/// // convert into dynamodb format
382/// impl From<MyStruct> for Item {
383///     fn from(value: MyStruct) -> Self {
384///         [
385///             ("some_attribute", AttributeValue::from(value.some_attribute)),
386///         ].into()
387///     }
388/// }
389///
390/// // convert from dynamodb format
391/// impl TryFrom<Item> for MyStruct {
392///     type Error = String;
393///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
394///         Ok(Self {
395///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
396///         })
397///     }
398/// }
399///
400/// let item: Item = MyStruct { some_attribute: "some value".to_string() }.into();
401/// ```
402#[derive(Debug, Serialize, Deserialize)]
403pub struct Item {
404    /// The item object
405    #[serde(flatten)]
406    pub attributes: HashMap<String, AttributeValue>,
407}
408
409/// A value within the item object
410#[derive(Debug, Serialize, Deserialize)]
411pub enum AttributeValue {
412    /// A B value
413    #[serde(rename = "B")]
414    Binary(String),
415    /// A BOOL value
416    #[serde(rename = "BOOL")]
417    Boolean(bool),
418    /// A BS value
419    #[serde(rename = "BS")]
420    BinarySet(Vec<String>),
421    /// An L value
422    #[serde(rename = "L")]
423    List(Vec<AttributeValue>),
424    /// An M value
425    #[serde(rename = "M")]
426    Map(HashMap<String, AttributeValue>),
427    /// An N value
428    #[serde(rename = "N")]
429    Number(String),
430    /// An NS value
431    #[serde(rename = "NS")]
432    NumberSet(Vec<String>),
433    /// A NULL value
434    #[serde(rename = "NULL")]
435    Null(bool),
436    /// An S value
437    #[serde(rename = "S")]
438    String(String),
439    /// An SS value
440    #[serde(rename = "SS")]
441    StringSet(Vec<String>),
442}
443
444impl AttributeValue {
445    fn type_name(&self) -> String {
446        match self {
447            AttributeValue::Binary(_) => "Binary".to_string(),
448            AttributeValue::Boolean(_) => "Boolean".to_string(),
449            AttributeValue::BinarySet(_) => "BinarySet".to_string(),
450            AttributeValue::List(_) => "List".to_string(),
451            AttributeValue::Map(_) => "Map".to_string(),
452            AttributeValue::Number(_) => "Number".to_string(),
453            AttributeValue::NumberSet(_) => "NumberSet".to_string(),
454            AttributeValue::Null(_) => "Null".to_string(),
455            AttributeValue::String(_) => "String".to_string(),
456            AttributeValue::StringSet(_) => "StringSet".to_string(),
457        }
458    }
459}
460
461/// An error occurred while converting from an AttributeValue.
462#[derive(Debug, thiserror::Error)]
463pub enum ConversionError {
464    /// The AttributeValue was not of the expected type.
465    #[error("Attribute was not of expected type. Expected: {expected}, Actual: {actual}")]
466    WrongType {
467        /// The expected AttributeValue type.
468        expected: String,
469        /// The actual AttributeValue type.
470        actual: String,
471    },
472}
473
474/// An error occurred while converting from an AttributeValue to a numeric type.
475#[derive(Debug, thiserror::Error)]
476pub enum NumericConversionError {
477    /// The AttributeValue was not of the expected type.
478    #[error("Attribute was not of expected type. Expected: {expected}, Actual: {actual}")]
479    WrongType {
480        /// The expected AttributeValue type.
481        expected: String,
482        /// The actual AttributeValue type.
483        actual: String,
484    },
485    /// Failed to parse an integer value.
486    #[error("ParseInt error: {cause}")]
487    ParseInt {
488        /// The underlying parse error.
489        #[from]
490        cause: std::num::ParseIntError,
491    },
492}
493
494/// An error occurred while converting from an AttributeValue to Bytes.
495#[derive(Debug, thiserror::Error)]
496pub enum BinaryConversionError {
497    /// The AttributeValue was not of the expected type.
498    #[error("Attribute was not of expected type. Expected: {expected}, Actual: {actual}")]
499    WrongType {
500        /// The expected AttributeValue type.
501        expected: String,
502        /// The actual AttributeValue type.
503        actual: String,
504    },
505    /// The AttributeValue did not contain valid base64.
506    #[error("Decode error: {cause}")]
507    Decode {
508        /// The underlying decode error.
509        #[from]
510        cause: base64::DecodeError,
511    },
512}
513
514impl From<String> for AttributeValue {
515    fn from(value: String) -> Self {
516        AttributeValue::String(value)
517    }
518}
519impl From<&str> for AttributeValue {
520    fn from(value: &str) -> Self {
521        AttributeValue::String(value.to_string())
522    }
523}
524impl From<bool> for AttributeValue {
525    fn from(value: bool) -> Self {
526        AttributeValue::Boolean(value)
527    }
528}
529impl From<Vec<AttributeValue>> for AttributeValue {
530    fn from(value: Vec<AttributeValue>) -> Self {
531        AttributeValue::List(value)
532    }
533}
534impl<S> From<HashMap<S, AttributeValue>> for AttributeValue
535where
536    S: Into<String>,
537{
538    fn from(value: HashMap<S, AttributeValue>) -> Self {
539        AttributeValue::Map(value.into_iter().map(|(k, v)| (k.into(), v)).collect())
540    }
541}
542impl From<i64> for AttributeValue {
543    fn from(value: i64) -> Self {
544        AttributeValue::Number(value.to_string())
545    }
546}
547impl From<Vec<u8>> for AttributeValue {
548    fn from(value: Vec<u8>) -> Self {
549        AttributeValue::Binary(base64::engine::general_purpose::STANDARD_NO_PAD.encode(value))
550    }
551}
552
553impl TryFrom<AttributeValue> for String {
554    type Error = ConversionError;
555    fn try_from(value: AttributeValue) -> Result<Self, Self::Error> {
556        match value {
557            AttributeValue::String(s) => Ok(s),
558            _ => Err(ConversionError::WrongType {
559                actual: value.type_name(),
560                expected: "String".to_string(),
561            }),
562        }
563    }
564}
565impl TryFrom<AttributeValue> for bool {
566    type Error = ConversionError;
567    fn try_from(value: AttributeValue) -> Result<Self, Self::Error> {
568        match value {
569            AttributeValue::Boolean(b) => Ok(b),
570            _ => Err(ConversionError::WrongType {
571                actual: value.type_name(),
572                expected: "Boolean".to_string(),
573            }),
574        }
575    }
576}
577impl TryFrom<AttributeValue> for i64 {
578    type Error = NumericConversionError;
579    fn try_from(value: AttributeValue) -> Result<Self, Self::Error> {
580        match value {
581            AttributeValue::Number(n) => n.parse::<i64>().map_err(NumericConversionError::from),
582            _ => Err(NumericConversionError::WrongType {
583                actual: value.type_name(),
584                expected: "Number".to_string(),
585            }),
586        }
587    }
588}
589impl TryFrom<AttributeValue> for Vec<u8> {
590    type Error = BinaryConversionError;
591    fn try_from(value: AttributeValue) -> Result<Self, Self::Error> {
592        match value {
593            AttributeValue::Binary(b) => base64::engine::general_purpose::STANDARD_NO_PAD
594                .decode(b)
595                .map_err(BinaryConversionError::from),
596            _ => Err(BinaryConversionError::WrongType {
597                actual: value.type_name(),
598                expected: "Binary".to_string(),
599            }),
600        }
601    }
602}
603
604impl<I, S, V> From<I> for Item
605where
606    I: IntoIterator<Item = (S, V)>,
607    S: Into<String>,
608    V: Into<AttributeValue>,
609{
610    fn from(value: I) -> Self {
611        Item {
612            attributes: value
613                .into_iter()
614                .map(|(k, v)| (k.into(), v.into()))
615                .collect(),
616        }
617    }
618}