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
253#[derive(Debug, serde::Serialize, serde::Deserialize)]
254pub enum KeyValue {
255 /// S value
256 #[serde(rename = "S")]
257 String(String),
258 /// N value
259 #[serde(rename = "N")]
260 Number(i64),
261 /// B value
262 #[serde(rename = "B")]
263 Binary(Vec<u8>),
264}
265
266impl<K, V> From<(K, V)> for Key
267where
268 K: Into<String>,
269 V: Into<KeyValue>,
270{
271 fn from((k, v): (K, V)) -> Self {
272 Key::Hash {
273 key: k.into(),
274 value: v.into(),
275 }
276 }
277}
278
279impl From<Key> for Vec<host::aws_ddb::KeyAttribute> {
280 fn from(value: Key) -> Self {
281 match value {
282 Key::Hash { key, value } => vec![host::aws_ddb::KeyAttribute {
283 name: key,
284 value: value.into(),
285 }],
286 Key::HashRange {
287 hash_key,
288 hash_value,
289 range_key,
290 range_value,
291 } => vec![
292 host::aws_ddb::KeyAttribute {
293 name: hash_key,
294 value: hash_value.into(),
295 },
296 host::aws_ddb::KeyAttribute {
297 name: range_key,
298 value: range_value.into(),
299 },
300 ],
301 }
302 }
303}
304
305impl From<String> for KeyValue {
306 fn from(value: String) -> Self {
307 KeyValue::String(value)
308 }
309}
310impl From<&str> for KeyValue {
311 fn from(value: &str) -> Self {
312 KeyValue::String(value.to_string())
313 }
314}
315impl From<i64> for KeyValue {
316 fn from(value: i64) -> Self {
317 KeyValue::Number(value)
318 }
319}
320impl From<Vec<u8>> for KeyValue {
321 fn from(value: Vec<u8>) -> Self {
322 KeyValue::Binary(value)
323 }
324}
325impl From<&[u8]> for KeyValue {
326 fn from(value: &[u8]) -> Self {
327 KeyValue::Binary(value.to_vec())
328 }
329}
330impl From<KeyValue> for host::aws_ddb::KeyValue {
331 fn from(value: KeyValue) -> Self {
332 match value {
333 KeyValue::String(s) => host::aws_ddb::KeyValue::S(s),
334 KeyValue::Number(n) => host::aws_ddb::KeyValue::N(n.to_string()),
335 KeyValue::Binary(b) => host::aws_ddb::KeyValue::B(
336 base64::engine::general_purpose::STANDARD_NO_PAD.encode(b),
337 ),
338 }
339 }
340}
341
342impl From<host::aws_ddb::DdbError> for crate::Error {
343 fn from(e: host::aws_ddb::DdbError) -> Self {
344 match e {
345 host::aws_ddb::DdbError::Unauthorized(u) => Self::MessageError(u),
346 host::aws_ddb::DdbError::Malformed(s) => Self::MessageError(s),
347 host::aws_ddb::DdbError::Other(o) => Self::MessageError(o),
348 }
349 }
350}
351
352/// dynamodb-formatted json looks something like this:
353/// ```json
354/// {
355/// "profile_picture": { "B": "base64 string" },
356/// "is_valid": { "BOOL": true },
357/// "pictures": { "BS": ["base64 1", "base64 2"] },
358/// "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
359/// "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
360/// "age": { "N": "23" },
361/// "favorite_birthdays": { "NS": ["17", "25"] },
362/// "children": { "NULL": true },
363/// "name": { "S": "arthur" },
364/// "friends": { "SS": ["bob", "alice"] }
365/// }
366/// ```
367/// This stuff exists mostly because WIT maintainers consider list<t> to be dependent on t,
368/// which causes much consternation with regard to serialization. Eventually they will
369/// likely work it out like json, protocol buffers, msgpack, and many other serialization
370/// formats before it.
371///
372/// Examples:
373/// ________
374/// Basic explicit lists:
375/// ```rust
376/// use momento_functions_host::aws::ddb::Item;
377/// let item: Item = vec![("some key", "some value")].into();
378/// let item: Item = vec![("some key", 42)].into();
379/// ```
380/// ________
381/// Custom bound types:
382/// ```rust
383/// use momento_functions_host::aws::ddb::{AttributeValue, Item};
384/// use momento_functions_host::Error;
385/// struct MyStruct {
386/// some_attribute: String,
387/// }
388///
389/// // convert into dynamodb format
390/// impl From<MyStruct> for Item {
391/// fn from(value: MyStruct) -> Self {
392/// [
393/// ("some_attribute", AttributeValue::from(value.some_attribute)),
394/// ].into()
395/// }
396/// }
397///
398/// // convert from dynamodb format
399/// impl TryFrom<Item> for MyStruct {
400/// type Error = Error;
401/// fn try_from(mut value: Item) -> Result<Self, Self::Error> {
402/// Ok(Self {
403/// some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
404/// })
405/// }
406/// }
407///
408/// let item: Item = MyStruct { some_attribute: "some value".to_string() }.into();
409/// ```
410#[derive(Debug, Serialize, Deserialize)]
411pub struct Item {
412 /// The item object
413 #[serde(flatten)]
414 pub attributes: HashMap<String, AttributeValue>,
415}
416
417/// A value within the item object
418#[derive(Debug, Serialize, Deserialize)]
419pub enum AttributeValue {
420 /// A B value
421 #[serde(rename = "B")]
422 Binary(String),
423 /// A BOOL value
424 #[serde(rename = "BOOL")]
425 Boolean(bool),
426 /// A BS value
427 #[serde(rename = "BS")]
428 BinarySet(Vec<String>),
429 /// An L value
430 #[serde(rename = "L")]
431 List(Vec<AttributeValue>),
432 /// An M value
433 #[serde(rename = "M")]
434 Map(HashMap<String, AttributeValue>),
435 /// An N value
436 #[serde(rename = "N")]
437 Number(String),
438 /// An NS value
439 #[serde(rename = "NS")]
440 NumberSet(Vec<String>),
441 /// A NULL value
442 #[serde(rename = "NULL")]
443 Null(bool),
444 /// An S value
445 #[serde(rename = "S")]
446 String(String),
447 /// An SS value
448 #[serde(rename = "SS")]
449 StringSet(Vec<String>),
450}
451
452impl From<String> for AttributeValue {
453 fn from(value: String) -> Self {
454 AttributeValue::String(value)
455 }
456}
457impl From<&str> for AttributeValue {
458 fn from(value: &str) -> Self {
459 AttributeValue::String(value.to_string())
460 }
461}
462impl From<bool> for AttributeValue {
463 fn from(value: bool) -> Self {
464 AttributeValue::Boolean(value)
465 }
466}
467impl From<Vec<AttributeValue>> for AttributeValue {
468 fn from(value: Vec<AttributeValue>) -> Self {
469 AttributeValue::List(value)
470 }
471}
472impl<S> From<HashMap<S, AttributeValue>> for AttributeValue
473where
474 S: Into<String>,
475{
476 fn from(value: HashMap<S, AttributeValue>) -> Self {
477 AttributeValue::Map(value.into_iter().map(|(k, v)| (k.into(), v)).collect())
478 }
479}
480impl From<i64> for AttributeValue {
481 fn from(value: i64) -> Self {
482 AttributeValue::Number(value.to_string())
483 }
484}
485impl From<Vec<u8>> for AttributeValue {
486 fn from(value: Vec<u8>) -> Self {
487 AttributeValue::Binary(base64::engine::general_purpose::STANDARD_NO_PAD.encode(value))
488 }
489}
490impl TryFrom<AttributeValue> for String {
491 type Error = crate::Error;
492 fn try_from(value: AttributeValue) -> FunctionResult<Self> {
493 match value {
494 AttributeValue::String(s) => Ok(s),
495 _ => Err(Self::Error::MessageError("not a string".to_string())),
496 }
497 }
498}
499impl TryFrom<AttributeValue> for bool {
500 type Error = crate::Error;
501 fn try_from(value: AttributeValue) -> FunctionResult<Self> {
502 match value {
503 AttributeValue::Boolean(b) => Ok(b),
504 _ => Err(Self::Error::MessageError("not a bool".to_string())),
505 }
506 }
507}
508impl TryFrom<AttributeValue> for i64 {
509 type Error = crate::Error;
510 fn try_from(value: AttributeValue) -> FunctionResult<Self> {
511 match value {
512 AttributeValue::Number(n) => n
513 .parse::<i64>()
514 .map_err(|e| Self::Error::MessageError(format!("invalid number: {e}"))),
515 _ => Err(Self::Error::MessageError("not a number".to_string())),
516 }
517 }
518}
519impl TryFrom<AttributeValue> for Vec<u8> {
520 type Error = crate::Error;
521 fn try_from(value: AttributeValue) -> FunctionResult<Self> {
522 match value {
523 AttributeValue::Binary(b) => base64::engine::general_purpose::STANDARD_NO_PAD
524 .decode(b)
525 .map_err(|e| Self::Error::MessageError(format!("invalid base64: {e}"))),
526 _ => Err(Self::Error::MessageError("not a binary".to_string())),
527 }
528 }
529}
530
531impl<I, S, V> From<I> for Item
532where
533 I: IntoIterator<Item = (S, V)>,
534 S: Into<String>,
535 V: Into<AttributeValue>,
536{
537 fn from(value: I) -> Self {
538 Item {
539 attributes: value
540 .into_iter()
541 .map(|(k, v)| (k.into(), v.into()))
542 .collect(),
543 }
544 }
545}