diff --git a/examples/basic-sdk/Cargo.toml b/examples/basic-sdk/Cargo.toml new file mode 100644 index 00000000..0ffea2da --- /dev/null +++ b/examples/basic-sdk/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "basic-sdk" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = "0.1" +aws-config = "0.54" +aws-sdk-s3 = "0.24" +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.136" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] } + +[dev-dependencies] +mockall = "0.11.3" +tokio-test = "0.4.2" \ No newline at end of file diff --git a/examples/basic-sdk/README.md b/examples/basic-sdk/README.md new file mode 100644 index 00000000..483f6951 --- /dev/null +++ b/examples/basic-sdk/README.md @@ -0,0 +1,21 @@ + +## basic-sdk example + +This is an sample function that uses the [AWS SDK](https://github.com/awslabs/aws-sdk-rust) to +list the contents of an S3 bucket specified by the invoker. It uses standard credentials as defined +in the function's execution role to make calls against S3. + +### Running Locally +You can use `cargo lambda watch` to spin up a local version of the function. This will automatically re-compile and restart +itself when it observes changes to the code. If you invoke `watch` with no other context then the function will not have +the environment variables necessary to supply on SDK calls. To get around this you can manually supply a credentials file +profile for the SDK to resolve and use in your function: +``` +AWS_PROFILE=my-profile cargo lambda watch +``` + +### Invoking +You can invoke by simply leveraging `cargo lambda invoke` with the payload expected by the function handler. +``` +cargo lambda invoke --data-ascii '{"bucket":"my-bucket"}' +``` diff --git a/examples/basic-sdk/src/main.rs b/examples/basic-sdk/src/main.rs new file mode 100644 index 00000000..5838d7c8 --- /dev/null +++ b/examples/basic-sdk/src/main.rs @@ -0,0 +1,140 @@ +use async_trait::async_trait; +use aws_sdk_s3::{output::ListObjectsV2Output, Client as S3Client}; +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +/// The request defines what bucket to list +#[derive(Deserialize)] +struct Request { + bucket: String, +} + +/// The response contains a Lambda-generated request ID and +/// the list of objects in the bucket. +#[derive(Serialize)] +struct Response { + req_id: String, + bucket: String, + objects: Vec, +} + +#[cfg_attr(test, mockall::automock)] +#[async_trait] +trait ListObjects { + async fn list_objects(&self, bucket: &str) -> Result; +} + +#[async_trait] +impl ListObjects for S3Client { + async fn list_objects(&self, bucket: &str) -> Result { + self.list_objects_v2().bucket(bucket).send().await.map_err(|e| e.into()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // this needs to be set to false, otherwise ANSI color codes will + // show up in a confusing manner in CloudWatch logs. + .with_ansi(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + let shared_config = aws_config::load_from_env().await; + let client = S3Client::new(&shared_config); + let client_ref = &client; + + let func = service_fn(move |event| async move { my_handler(event, client_ref).await }); + lambda_runtime::run(func).await?; + + Ok(()) +} + +async fn my_handler(event: LambdaEvent, client: &T) -> Result { + let bucket = event.payload.bucket; + + let objects_rsp = client.list_objects(&bucket).await?; + let objects: Vec<_> = objects_rsp + .contents() + .ok_or("missing objects in list-objects-v2 response")? + .into_iter() + .filter_map(|o| o.key().map(|k| k.to_string())) + .collect(); + + // prepare the response + let rsp = Response { + req_id: event.context.request_id, + bucket: bucket.clone(), + objects, + }; + + // return `Response` (it will be serialized to JSON automatically by the runtime) + Ok(rsp) +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_sdk_s3::model::Object; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::predicate::eq; + + #[tokio::test] + async fn response_is_good_for_good_bucket() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let mut mock_client = MockListObjects::default(); + mock_client + .expect_list_objects() + .with(eq("test-bucket")) + .returning(|_| { + Ok(ListObjectsV2Output::builder() + .contents(Object::builder().key("test-key-0").build()) + .contents(Object::builder().key("test-key-1").build()) + .contents(Object::builder().key("test-key-2").build()) + .build()) + }); + + let payload = Request { + bucket: "test-bucket".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event, &mock_client).await.unwrap(); + + let expected_keys = vec![ + "test-key-0".to_string(), + "test-key-1".to_string(), + "test-key-2".to_string(), + ]; + assert_eq!(result.req_id, "test-request-id".to_string()); + assert_eq!(result.bucket, "test-bucket".to_string()); + assert_eq!(result.objects, expected_keys); + } + + #[tokio::test] + async fn response_is_bad_for_bad_bucket() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let mut mock_client = MockListObjects::default(); + mock_client + .expect_list_objects() + .with(eq("unknown-bucket")) + .returning(|_| Err(Error::from("test-sdk-error"))); + + let payload = Request { + bucket: "unknown-bucket".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event, &mock_client).await; + assert!(result.is_err()); + } +}