diff --git a/Cargo.toml b/Cargo.toml index 8773eb9..9d86a37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,20 @@ aws-smithy-client = { version = "0.34", features = ["test-util"] } aws-smithy-http = "0.34" aws-types = "0.4" futures = { version = "0.3", features = ["std"] } -lambda_runtime = { version = "0.4", optional = true } -lambda_http = { version = "0.4", optional = true } -rayon = { version = "1.5", optional = true } serde = "1" serde_json = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.2", features = ["fmt", "json"] } tokio = { version = "1", features = ["full"] } +# Lambda +lambda_runtime = { version = "0.4", optional = true } +lambda_http = { version = "0.4", optional = true } +rayon = { version = "1.5", optional = true } + +# Container +rocket = { version = "0.5.0-rc.1", optional = true } + [dev-dependencies] # Only allow hardcoded credentials for unit tests aws-types = { version = "0.4", features = ["hardcoded-credentials"] } @@ -31,8 +36,9 @@ rand = "0.8" reqwest = { version = "0.11", features = ["json"] } [features] -default = ["lambda"] +default = ["lambda", "container"] lambda = ["lambda_runtime", "lambda_http", "rayon"] +container = ["rocket"] [[bin]] name = "delete-product" @@ -63,3 +69,8 @@ name = "dynamodb-streams" path = "src/bin/lambda/dynamodb-streams.rs" test = false required-features = ["lambda"] + +[[bin]] +name = "rust-products" +path = "src/bin/container/rust-products.rs" +required-features = ["container"] diff --git a/src/bin/container/rust-products.rs b/src/bin/container/rust-products.rs new file mode 100644 index 0000000..dee9477 --- /dev/null +++ b/src/bin/container/rust-products.rs @@ -0,0 +1,18 @@ +use products::{entrypoints::container::*, utils::*}; + +#[tokio::main] +async fn main() -> Result<(), rocket::Error> { + let store = get_store().await; + let config = Config::new(store); + + setup_tracing(); + + rocket::build() + .mount( + "/", + rocket::routes![get_products, get_product, put_product, delete_product], + ) + .manage(config) + .launch() + .await +} diff --git a/src/entrypoints/container.rs b/src/entrypoints/container.rs new file mode 100644 index 0000000..d2b995b --- /dev/null +++ b/src/entrypoints/container.rs @@ -0,0 +1,86 @@ +use crate::{domain, store}; +use rocket::State; +use serde_json::json; +use tracing::{error, info, instrument}; + +pub struct Config { + store: store::DynamoDBStore, +} + +impl Config { + pub fn new(store: store::DynamoDBStore) -> Self { + Self { store } + } +} + +#[rocket::delete("/")] +#[instrument(skip(state))] +pub async fn delete_product(state: &State, id: String) -> String { + + // Delete product + info!("Deleting product {}", id); + let res = domain::delete_product(&state.store, &id).await; + + match res { + Ok(_) => { + info!("Product {} deleted", id); + json!({ "message": "Product deleted" }) + } + Err(err) => { + error!("Error deleting the product {}: {}", id, err); + json!({ "message": "Failed to delete product" }) + } + } + .to_string() +} + +#[rocket::get("/")] +#[instrument(skip(state))] +pub async fn get_product(state: &State, id: String) -> String { + let res = domain::get_product(&state.store, &id).await; + + match res { + Ok(product) => json!(product), + Err(err) => { + error!("Error getting the product {}: {}", id, err); + json!({ "message": "Failed to get product" }) + } + } + .to_string() +} + +#[rocket::get("/")] +#[instrument(skip(state))] +pub async fn get_products(state: &State) -> String { + let res = domain::get_products(&state.store, None).await; + + match res { + Ok(res) => json!(res), + Err(err) => { + error!("Something went wrong: {:?}", err); + json!({ "message": format!("Something went wrong: {:?}", err) }) + } + } + .to_string() +} + +#[rocket::put("/", data = "")] +#[instrument(skip(state))] +pub async fn put_product(state: &State, id: String, product: String) -> String { + // TODO: Validate the product + let product = serde_json::from_str(&product).unwrap(); + + let res = domain::put_product(&state.store, &product).await; + + match res { + Ok(_) => { + info!("Created product {:?}", product.id); + json!({ "message": "Product created" }) + } + Err(err) => { + error!("Failed to create product {}: {}", product.id, err); + json!({ "message": "Failed to create product" }) + } + } + .to_string() +} diff --git a/src/entrypoints/mod.rs b/src/entrypoints/mod.rs index b6534a1..b7af959 100644 --- a/src/entrypoints/mod.rs +++ b/src/entrypoints/mod.rs @@ -1,2 +1,5 @@ #[cfg(feature = "lambda")] pub mod lambda; + +#[cfg(feature = "container")] +pub mod container; diff --git a/src/store/dynamodb/mod.rs b/src/store/dynamodb/mod.rs index 0f2acc3..53f19c7 100644 --- a/src/store/dynamodb/mod.rs +++ b/src/store/dynamodb/mod.rs @@ -17,27 +17,21 @@ use ext::AttributeValuesExt; /// We have to pass a generic type parameter `C` for the underlying client, /// restricted to something that implements the SmithyConnector trait so we can /// use it with both the actual AWS SDK client and a mock implementation. -pub struct DynamoDBStore { - client: Client, +pub struct DynamoDBStore { + client: Client, table_name: String, } -impl DynamoDBStore -where - C: aws_smithy_client::bounds::SmithyConnector, -{ - pub fn new(client: Client, table_name: String) -> DynamoDBStore { +impl DynamoDBStore { + pub fn new(client: Client, table_name: String) -> DynamoDBStore { DynamoDBStore { client, table_name } } } -impl Store for DynamoDBStore where C: aws_smithy_client::bounds::SmithyConnector {} +impl Store for DynamoDBStore {} #[async_trait] -impl StoreGetAll for DynamoDBStore -where - C: aws_smithy_client::bounds::SmithyConnector, -{ +impl StoreGetAll for DynamoDBStore { /// Get all items #[instrument(skip(self))] async fn all(&self, next: Option<&str>) -> Result { @@ -65,10 +59,7 @@ where } #[async_trait] -impl StoreGet for DynamoDBStore -where - C: aws_smithy_client::bounds::SmithyConnector, -{ +impl StoreGet for DynamoDBStore { /// Get item #[instrument(skip(self))] async fn get(&self, id: &str) -> Result, Error> { @@ -89,10 +80,7 @@ where } #[async_trait] -impl StorePut for DynamoDBStore -where - C: aws_smithy_client::bounds::SmithyConnector, -{ +impl StorePut for DynamoDBStore { /// Create or update an item #[instrument(skip(self))] async fn put(&self, product: &Product) -> Result<(), Error> { @@ -109,10 +97,7 @@ where } #[async_trait] -impl StoreDelete for DynamoDBStore -where - C: aws_smithy_client::bounds::SmithyConnector, -{ +impl StoreDelete for DynamoDBStore { /// Delete item #[instrument(skip(self))] async fn delete(&self, id: &str) -> Result<(), Error> { @@ -168,7 +153,7 @@ mod tests { use super::*; use crate::Error; use aws_sdk_dynamodb::{Client, Config, Credentials, Region}; - use aws_smithy_client::test_connection::TestConnection; + use aws_smithy_client::{erase::DynConnector, test_connection::TestConnection}; use aws_smithy_http::body::SdkBody; /// Config for mocking DynamoDB @@ -203,7 +188,8 @@ mod tests { .body(SdkBody::from(r#"{"Items": []}"#)) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); // WHEN getting all items @@ -229,7 +215,8 @@ mod tests { .body(SdkBody::from(r#"{"Items": [{"id": {"S": "1"}, "name": {"S": "test1"}, "price": {"N": "1.0"}}]}"#)) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); // WHEN getting all items @@ -264,7 +251,8 @@ mod tests { )) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); // WHEN getting all items @@ -293,7 +281,8 @@ mod tests { .body(SdkBody::from("{}")) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); // WHEN deleting an item @@ -318,7 +307,8 @@ mod tests { .body(SdkBody::from(r#"{"Item": {"id": {"S": "1"}, "name": {"S": "test1"}, "price": {"N": "1.0"}}}"#)) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); // WHEN getting an item @@ -351,7 +341,8 @@ mod tests { .body(SdkBody::from(r#"{"Attributes": {"id": {"S": "1"}, "name": {"S": "test1"}, "price": {"N": "1.5"}}}"#)) .unwrap(), )]); - let client = Client::from_conf_conn(get_mock_config().await, conn.clone()); + let client = + Client::from_conf_conn(get_mock_config().await, DynConnector::new(conn.clone())); let store = DynamoDBStore::new(client, "test".to_string()); let product = Product { id: "1".to_string(), diff --git a/src/utils.rs b/src/utils.rs index 342d261..cba4d07 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -12,7 +12,7 @@ pub fn setup_tracing() { /// Initialize a store #[instrument] -pub async fn get_store() -> impl store::Store { +pub async fn get_store() -> store::DynamoDBStore { // Get AWS Configuration let config = aws_config::load_from_env().await;