diff --git a/Cargo.toml b/Cargo.toml index 389156a..ee5f44c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,13 @@ tracing = "0.1" tracing-subscriber = { version = "0.2", features = ["fmt", "json"] } tokio = { version = "1", features = ["full"] } +[dependencies.serde_dynamo] +git = "https://github.com/sciguy16/serde_dynamo.git" +branch = "aws-0_5" +features = ["aws-sdk-dynamodb+0_6"] +version = "3.0.0-alpha" +optional = true + [dev-dependencies] # Only allow hardcoded credentials for unit tests aws-types = { version = "0.4", features = ["hardcoded-credentials"] } @@ -32,7 +39,7 @@ reqwest = { version = "0.11", features = ["json"] } [features] default = ["lambda"] -lambda = ["lambda_runtime", "lambda_http", "rayon"] +lambda = ["lambda_runtime", "lambda_http", "rayon", "serde_dynamo"] [[bin]] name = "delete-product" diff --git a/src/store/dynamodb/ext.rs b/src/store/dynamodb/ext.rs deleted file mode 100644 index 4b57213..0000000 --- a/src/store/dynamodb/ext.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! # Extension traits for `DynamoDbStore`. - -use aws_sdk_dynamodb::model::AttributeValue; -use std::collections::HashMap; - -/// Trait to extract concrete values from a DynamoDB item -/// -/// The DynamoDB client returns AttributeValues, which are enums that contain -/// the concrete values. This trait provides additional methods to the HashMap -/// to extract those values. -pub trait AttributeValuesExt { - fn get_s(&self, key: &str) -> Option; - fn get_n(&self, key: &str) -> Option; -} - -impl AttributeValuesExt for HashMap { - /// Return a string from a key - /// - /// E.g. if you run `get_s("id")` on a DynamoDB item structured like this, - /// you will retrieve the value `"foo"`. - /// - /// ```json - /// { - /// "id": { - /// "S": "foo" - /// } - /// } - /// ``` - fn get_s(&self, key: &str) -> Option { - Some(self.get(key)?.as_s().ok()?.to_owned()) - } - - /// Return a number from a key - /// - /// E.g. if you run `get_n("price")` on a DynamoDB item structured like this, - /// you will retrieve the value `10.0`. - /// - /// ```json - /// { - /// "price": { - /// "N": "10.0" - /// } - /// } - /// ``` - fn get_n(&self, key: &str) -> Option { - self.get(key)?.as_n().ok()?.parse::().ok() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn attributevalue_get_s() { - let mut item = HashMap::new(); - item.insert("id".to_owned(), AttributeValue::S("foo".to_owned())); - - assert_eq!(item.get_s("id"), Some("foo".to_owned())); - } - - #[test] - fn attributevalue_get_s_missing() { - let mut item = HashMap::new(); - item.insert("id".to_owned(), AttributeValue::S("foo".to_owned())); - - assert_eq!(item.get_s("foo"), None); - } - - #[test] - fn attributevalue_get_n() { - let mut item = HashMap::new(); - item.insert("price".to_owned(), AttributeValue::N("10.0".to_owned())); - - assert_eq!(item.get_n("price"), Some(10.0)); - } - - #[test] - fn attributevalue_get_n_missing() { - let mut item = HashMap::new(); - item.insert("price".to_owned(), AttributeValue::N("10.0".to_owned())); - - assert_eq!(item.get_n("foo"), None); - } -} diff --git a/src/store/dynamodb/mod.rs b/src/store/dynamodb/mod.rs index 877b964..cf97773 100644 --- a/src/store/dynamodb/mod.rs +++ b/src/store/dynamodb/mod.rs @@ -6,12 +6,9 @@ use super::{Store, StoreDelete, StoreGet, StoreGetAll, StorePut}; use crate::{Error, Product, ProductRange}; use async_trait::async_trait; use aws_sdk_dynamodb::{model::AttributeValue, Client}; -use std::collections::HashMap; +use serde_dynamo::aws_sdk_dynamodb_0_6::{from_attribute_value, from_item, from_items, to_item}; use tracing::{info, instrument}; -mod ext; -use ext::AttributeValuesExt; - /// DynamoDB store implementation. pub struct DynamoDBStore { client: Client, @@ -42,14 +39,15 @@ impl StoreGetAll for DynamoDBStore { let res = req.send().await?; // Build response - let products = match res.items { - Some(items) => items - .into_iter() - .map(|v| v.try_into()) - .collect::, Error>>()?, + let products: Vec = match res.items { + Some(items) => from_items(items).map_err(|_| + // TODO: Find out correct error from underlying error? + Error::InternalError("Missing name"))?, None => Vec::default(), }; - let next = res.last_evaluated_key.map(|m| m.get_s("id").unwrap()); + let next = res + .last_evaluated_key + .map(|m| from_attribute_value(m["id"].clone()).unwrap()); Ok(ProductRange { products, next }) } } @@ -69,7 +67,8 @@ impl StoreGet for DynamoDBStore { .await?; Ok(match res.item { - Some(item) => Some(item.try_into()?), + // TODO: Find out correct error from underlying error? + Some(item) => from_item(item).map_err(|_| Error::InternalError("Missing name"))?, None => None, }) } @@ -84,7 +83,8 @@ impl StorePut for DynamoDBStore { self.client .put_item() .table_name(&self.table_name) - .set_item(Some(product.into())) + // TODO: Can this fail? + .set_item(Some(to_item(product).unwrap())) .send() .await?; @@ -109,41 +109,6 @@ impl StoreDelete for DynamoDBStore { } } -impl From<&Product> for HashMap { - /// Convert a &Product into a DynamoDB item - fn from(value: &Product) -> HashMap { - let mut retval = HashMap::new(); - retval.insert("id".to_owned(), AttributeValue::S(value.id.clone())); - retval.insert("name".to_owned(), AttributeValue::S(value.name.clone())); - retval.insert( - "price".to_owned(), - AttributeValue::N(format!("{:}", value.price)), - ); - - retval - } -} -impl TryFrom> for Product { - type Error = Error; - - /// Try to convert a DynamoDB item into a Product - /// - /// This could fail as the DynamoDB item might be missing some fields. - fn try_from(value: HashMap) -> Result { - Ok(Product { - id: value - .get_s("id") - .ok_or(Error::InternalError("Missing id"))?, - name: value - .get_s("name") - .ok_or(Error::InternalError("Missing name"))?, - price: value - .get_n("price") - .ok_or(Error::InternalError("Missing price"))?, - }) - } -} - #[cfg(test)] mod tests { use super::*; @@ -151,6 +116,7 @@ mod tests { use aws_sdk_dynamodb::{Client, Config, Credentials, Region}; use aws_smithy_client::{erase::DynConnector, test_connection::TestConnection}; use aws_smithy_http::body::SdkBody; + use std::collections::HashMap; /// Config for mocking DynamoDB async fn get_mock_config() -> Config { @@ -368,7 +334,7 @@ mod tests { value.insert("name".to_owned(), AttributeValue::S("name".to_owned())); value.insert("price".to_owned(), AttributeValue::N("1.0".to_owned())); - let product = Product::try_from(value).unwrap(); + let product: Product = from_item(value).unwrap(); assert_eq!(product.id, "id"); assert_eq!(product.name, "name"); assert_eq!(product.price, 1.0); @@ -382,7 +348,7 @@ mod tests { price: 1.5, }; - let value: HashMap = (&product).into(); + let value: HashMap = to_item(product).unwrap(); assert_eq!(value.get("id").unwrap().as_s().unwrap(), "id"); assert_eq!(value.get("name").unwrap().as_s().unwrap(), "name"); assert_eq!(value.get("price").unwrap().as_n().unwrap(), "1.5");