Skip to content

Commit bce880a

Browse files
committed
basic-sdk example
adds an example for a basic Lambda function that calls S3. this demonstrates the best practice of instantiating an SDK client during function initialization (main method) and reusing that client across multiple invokes.
1 parent 0f6e2a2 commit bce880a

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

examples/basic-sdk/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "basic-sdk"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
async-trait = "0.1"
10+
aws-config = "0.54"
11+
aws-sdk-s3 = "0.24"
12+
lambda_runtime = { path = "../../lambda-runtime" }
13+
serde = "1.0.136"
14+
tokio = { version = "1", features = ["macros"] }
15+
tracing = { version = "0.1", features = ["log"] }
16+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] }
17+
18+
[dev-dependencies]
19+
mockall = "0.11.3"
20+
tokio-test = "0.4.2"

examples/basic-sdk/src/main.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// This example requires the following input to succeed:
2+
// { "bucket": "bucket-to-list" }
3+
4+
use async_trait::async_trait;
5+
use aws_sdk_s3::{output::ListObjectsV2Output, Client as S3Client};
6+
use lambda_runtime::{service_fn, Error, LambdaEvent};
7+
use serde::{Deserialize, Serialize};
8+
9+
/// The request defines what bucket to list
10+
#[derive(Deserialize)]
11+
struct Request {
12+
bucket: String,
13+
}
14+
15+
/// The response contains a Lambda-generated request ID and
16+
/// the list of objects in the bucket.
17+
#[derive(Serialize)]
18+
struct Response {
19+
req_id: String,
20+
bucket: String,
21+
objects: Vec<String>,
22+
}
23+
24+
#[cfg_attr(test, mockall::automock)]
25+
#[async_trait]
26+
trait ListObjects {
27+
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error>;
28+
}
29+
30+
#[async_trait]
31+
impl ListObjects for S3Client {
32+
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error> {
33+
self.list_objects_v2().bucket(bucket).send().await.map_err(|e| e.into())
34+
}
35+
}
36+
37+
#[tokio::main]
38+
async fn main() -> Result<(), Error> {
39+
// required to enable CloudWatch error logging by the runtime
40+
tracing_subscriber::fmt()
41+
.with_max_level(tracing::Level::INFO)
42+
// disable printing the name of the module in every log line.
43+
.with_target(false)
44+
// this needs to be set to false, otherwise ANSI color codes will
45+
// show up in a confusing manner in CloudWatch logs.
46+
.with_ansi(false)
47+
// disabling time is handy because CloudWatch will add the ingestion time.
48+
.without_time()
49+
.init();
50+
51+
let shared_config = aws_config::load_from_env().await;
52+
let client = S3Client::new(&shared_config);
53+
let client_ref = &client;
54+
55+
let func = service_fn(move |event| async move { my_handler(event, client_ref).await });
56+
lambda_runtime::run(func).await?;
57+
58+
Ok(())
59+
}
60+
61+
async fn my_handler<T: ListObjects>(event: LambdaEvent<Request>, client: &T) -> Result<Response, Error> {
62+
let bucket = event.payload.bucket;
63+
64+
let objects_rsp = client.list_objects(&bucket).await?;
65+
let objects: Vec<_> = objects_rsp
66+
.contents()
67+
.ok_or("missing objects in list-objects-v2 response")?
68+
.into_iter()
69+
.filter_map(|o| o.key().map(|k| k.to_string()))
70+
.collect();
71+
72+
// prepare the response
73+
let rsp = Response {
74+
req_id: event.context.request_id,
75+
bucket: bucket.clone(),
76+
objects,
77+
};
78+
79+
// return `Response` (it will be serialized to JSON automatically by the runtime)
80+
Ok(rsp)
81+
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
use aws_sdk_s3::model::Object;
87+
use lambda_runtime::{Context, LambdaEvent};
88+
use mockall::predicate::eq;
89+
90+
#[tokio::test]
91+
async fn response_is_good_for_good_bucket() {
92+
let mut context = Context::default();
93+
context.request_id = "test-request-id".to_string();
94+
95+
let mut mock_client = MockListObjects::default();
96+
mock_client
97+
.expect_list_objects()
98+
.with(eq("test-bucket"))
99+
.returning(|_| {
100+
Ok(ListObjectsV2Output::builder()
101+
.contents(Object::builder().key("test-key-0").build())
102+
.contents(Object::builder().key("test-key-1").build())
103+
.contents(Object::builder().key("test-key-2").build())
104+
.build())
105+
});
106+
107+
let payload = Request {
108+
bucket: "test-bucket".to_string(),
109+
};
110+
let event = LambdaEvent { payload, context };
111+
112+
let result = my_handler(event, &mock_client).await.unwrap();
113+
114+
let expected_keys = vec![
115+
"test-key-0".to_string(),
116+
"test-key-1".to_string(),
117+
"test-key-2".to_string(),
118+
];
119+
assert_eq!(result.req_id, "test-request-id".to_string());
120+
assert_eq!(result.bucket, "test-bucket".to_string());
121+
assert_eq!(result.objects, expected_keys);
122+
}
123+
124+
#[tokio::test]
125+
async fn response_is_bad_for_bad_bucket() {
126+
let mut context = Context::default();
127+
context.request_id = "test-request-id".to_string();
128+
129+
let mut mock_client = MockListObjects::default();
130+
mock_client
131+
.expect_list_objects()
132+
.with(eq("unknown-bucket"))
133+
.returning(|_| Err(Error::from("test-sdk-error")));
134+
135+
let payload = Request {
136+
bucket: "unknown-bucket".to_string(),
137+
};
138+
let event = LambdaEvent { payload, context };
139+
140+
let result = my_handler(event, &mock_client).await;
141+
assert!(result.is_err());
142+
}
143+
}

0 commit comments

Comments
 (0)