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}