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