Skip to content

basic-sdk example #619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/basic-sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 21 additions & 0 deletions examples/basic-sdk/README.md
Original file line number Diff line number Diff line change
@@ -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"}'
```
140 changes: 140 additions & 0 deletions examples/basic-sdk/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[cfg_attr(test, mockall::automock)]
#[async_trait]
trait ListObjects {
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error>;
}

#[async_trait]
impl ListObjects for S3Client {
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error> {
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<T: ListObjects>(event: LambdaEvent<Request>, client: &T) -> Result<Response, Error> {
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());
}
}