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}