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