Skip to content

Commit 079087d

Browse files
authored
Add builder pattern support for event response types (#1090)
* Add builder pattern support for event response types Add derived builders to events. Builders are conditionally compiled with the "builders" feature flag.
1 parent 5fc9137 commit 079087d

File tree

78 files changed

+1343
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1343
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ members = [
99
"lambda-events",
1010
]
1111

12-
exclude = ["examples"]
12+
exclude = ["examples","lambda-events/lambda-events-examples"]
1313

1414
[workspace.dependencies]
1515
base64 = "0.22"

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,80 @@ By default, the log level to emit events is `INFO`. Log at `TRACE` level for mor
449449

450450
This project includes Lambda event struct definitions, [`aws_lambda_events`](https://crates.io/crates/aws_lambda_events). This crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well.
451451

452+
### Builder pattern for event responses
453+
454+
The `aws_lambda_events` crate provides an optional `builders` feature that adds builder pattern support for constructing event responses. This is particularly useful when working with custom context types that don't implement `Default`.
455+
456+
Enable the builders feature in your `Cargo.toml`:
457+
458+
```toml
459+
[dependencies]
460+
aws_lambda_events = { version = "*", features = ["builders"] }
461+
```
462+
463+
Example with API Gateway custom authorizers:
464+
465+
```rust
466+
use aws_lambda_events::event::apigw::{
467+
ApiGatewayV2CustomAuthorizerSimpleResponseBuilder,
468+
ApiGatewayV2CustomAuthorizerV2Request,
469+
};
470+
use lambda_runtime::{Error, LambdaEvent};
471+
472+
struct MyContext {
473+
user_id: String,
474+
permissions: Vec<String>,
475+
}
476+
477+
async fn handler(
478+
event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
479+
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContext>, Error> {
480+
let context = MyContext {
481+
user_id: "user-123".to_string(),
482+
permissions: vec!["read".to_string()],
483+
};
484+
485+
let response = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::default()
486+
.is_authorized(true)
487+
.context(context)
488+
.build()?;
489+
490+
Ok(response)
491+
}
492+
```
493+
494+
Example with SQS batch responses:
495+
496+
```rust
497+
use aws_lambda_events::event::sqs::{
498+
BatchItemFailureBuilder,
499+
SqsBatchResponseBuilder,
500+
SqsEvent,
501+
};
502+
use lambda_runtime::{Error, LambdaEvent};
503+
504+
async fn handler(event: LambdaEvent<SqsEvent>) -> Result<SqsBatchResponse, Error> {
505+
let mut failures = Vec::new();
506+
507+
for record in event.payload.records {
508+
if let Err(_) = process_record(&record).await {
509+
let failure = BatchItemFailureBuilder::default()
510+
.item_identifier(record.message_id.unwrap())
511+
.build()?;
512+
failures.push(failure);
513+
}
514+
}
515+
516+
let response = SqsBatchResponseBuilder::default()
517+
.batch_item_failures(failures)
518+
.build()?;
519+
520+
Ok(response)
521+
}
522+
```
523+
524+
See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples.
525+
452526
### Custom event objects
453527

454528
To serialize and deserialize events and responses, we suggest using the [`serde`](https://github.com/serde-rs/serde) library. To receive custom events, annotate your structure with Serde's macros:

lambda-events/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ edition = "2021"
2020
base64 = { workspace = true }
2121
bytes = { workspace = true, features = ["serde"], optional = true }
2222
chrono = { workspace = true, optional = true }
23+
bon = { version = "3", optional = true }
2324
flate2 = { version = "1.0.24", optional = true }
2425
http = { workspace = true, optional = true }
2526
http-body = { workspace = true, optional = true }
@@ -126,6 +127,7 @@ documentdb = []
126127
eventbridge = ["chrono", "serde_with"]
127128

128129
catch-all-fields = []
130+
builders = ["bon"]
129131

130132
[package.metadata.docs.rs]
131133
all-features = true

lambda-events/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,50 @@ This crate divides all Lambda Events into features named after the service that
2727
cargo add aws_lambda_events --no-default-features --features apigw,alb
2828
```
2929

30+
### Builder pattern support
31+
32+
The crate provides an optional `builders` feature that adds builder pattern support for event types. This enables type-safe, immutable construction of event responses with a clean, ergonomic API.
33+
34+
Enable the builders feature:
35+
36+
```
37+
cargo add aws_lambda_events --features builders
38+
```
39+
40+
Example using builders with API Gateway custom authorizers:
41+
42+
```rust
43+
use aws_lambda_events::event::apigw::{
44+
ApiGatewayV2CustomAuthorizerSimpleResponse,
45+
ApiGatewayV2CustomAuthorizerV2Request,
46+
};
47+
use lambda_runtime::{Error, LambdaEvent};
48+
49+
// Context type without Default implementation
50+
struct MyContext {
51+
user_id: String,
52+
permissions: Vec<String>,
53+
}
54+
55+
async fn handler(
56+
event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
57+
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContext>, Error> {
58+
let context = MyContext {
59+
user_id: "user-123".to_string(),
60+
permissions: vec!["read".to_string()],
61+
};
62+
63+
let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
64+
.is_authorized(true)
65+
.context(context)
66+
.build();
67+
68+
Ok(response)
69+
}
70+
```
71+
72+
See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples.
73+
3074
## History
3175

3276
The AWS Lambda Events crate was created by [Christian Legnitto](https://github.com/LegNeato). Without all his work and dedication, this project could have not been possible.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "lambda-events-examples"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
aws_lambda_events = { path = "..", features = ["builders"] }
9+
lambda_runtime = { path = "../../lambda-runtime" }
10+
serde = { version = "1", features = ["derive"] }
11+
serde_json = "1"
12+
chrono = { version = "0.4", default-features = false, features = ["clock"] }
13+
serde_dynamo = "4"
14+
15+
[[example]]
16+
name = "comprehensive-builders"
17+
path = "examples/comprehensive-builders.rs"
18+
19+
[[example]]
20+
name = "lambda-runtime-authorizer-builder"
21+
path = "examples/lambda-runtime-authorizer-builder.rs"
22+
23+
[[example]]
24+
name = "lambda-runtime-sqs-batch-builder"
25+
path = "examples/lambda-runtime-sqs-batch-builder.rs"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Example demonstrating builder pattern usage for AWS Lambda events
2+
use aws_lambda_events::event::{
3+
dynamodb::{Event as DynamoDbEvent, EventRecord as DynamoDbEventRecord, StreamRecord},
4+
kinesis::{KinesisEvent, KinesisEventRecord, KinesisRecord, KinesisEncryptionType},
5+
s3::{S3Event, S3EventRecord, S3Entity, S3Bucket, S3Object, S3RequestParameters, S3UserIdentity},
6+
secretsmanager::SecretsManagerSecretRotationEvent,
7+
sns::{SnsEvent, SnsRecord, SnsMessage},
8+
sqs::{SqsEvent, SqsMessage},
9+
};
10+
use std::collections::HashMap;
11+
12+
fn main() {
13+
// S3 Event - Object storage notifications with nested structures
14+
let s3_record = S3EventRecord::builder()
15+
.event_time(chrono::Utc::now())
16+
.principal_id(S3UserIdentity::builder().build())
17+
.request_parameters(S3RequestParameters::builder().build())
18+
.response_elements(HashMap::new())
19+
.s3(S3Entity::builder()
20+
.bucket(S3Bucket::builder().name("my-bucket".to_string()).build())
21+
.object(S3Object::builder().key("file.txt".to_string()).size(1024).build())
22+
.build())
23+
.build();
24+
let _s3_event = S3Event::builder().records(vec![s3_record]).build();
25+
26+
// Kinesis Event - Stream processing with data
27+
let kinesis_record = KinesisEventRecord::builder()
28+
.kinesis(KinesisRecord::builder()
29+
.data(serde_json::from_str("\"SGVsbG8gV29ybGQ=\"").unwrap())
30+
.partition_key("key-1".to_string())
31+
.sequence_number("12345".to_string())
32+
.approximate_arrival_timestamp(serde_json::from_str("1234567890.0").unwrap())
33+
.encryption_type(KinesisEncryptionType::None)
34+
.build())
35+
.build();
36+
let _kinesis_event = KinesisEvent::builder().records(vec![kinesis_record]).build();
37+
38+
// DynamoDB Event - Database change streams with item data
39+
let mut keys = HashMap::new();
40+
keys.insert("id".to_string(), serde_dynamo::AttributeValue::S("123".to_string()));
41+
42+
let dynamodb_record = DynamoDbEventRecord::builder()
43+
.aws_region("us-east-1".to_string())
44+
.change(StreamRecord::builder()
45+
.approximate_creation_date_time(chrono::Utc::now())
46+
.keys(keys.into())
47+
.new_image(HashMap::new().into())
48+
.old_image(HashMap::new().into())
49+
.size_bytes(100)
50+
.build())
51+
.event_id("event-123".to_string())
52+
.event_name("INSERT".to_string())
53+
.build();
54+
let _dynamodb_event = DynamoDbEvent::builder().records(vec![dynamodb_record]).build();
55+
56+
// SNS Event - Pub/sub messaging with message details
57+
let sns_record = SnsRecord::builder()
58+
.event_source("aws:sns".to_string())
59+
.event_version("1.0".to_string())
60+
.event_subscription_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string())
61+
.sns(SnsMessage::builder()
62+
.message("Hello from SNS".to_string())
63+
.sns_message_type("Notification".to_string())
64+
.message_id("msg-123".to_string())
65+
.topic_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string())
66+
.timestamp(chrono::Utc::now())
67+
.signature_version("1".to_string())
68+
.signature("sig".to_string())
69+
.signing_cert_url("https://cert.url".to_string())
70+
.unsubscribe_url("https://unsub.url".to_string())
71+
.message_attributes(HashMap::new())
72+
.build())
73+
.build();
74+
let _sns_event = SnsEvent::builder().records(vec![sns_record]).build();
75+
76+
// SQS Event - Queue messaging with attributes
77+
let mut attrs = HashMap::new();
78+
attrs.insert("ApproximateReceiveCount".to_string(), "1".to_string());
79+
attrs.insert("SentTimestamp".to_string(), "1234567890".to_string());
80+
81+
let sqs_message = SqsMessage::builder()
82+
.attributes(attrs)
83+
.message_attributes(HashMap::new())
84+
.body("message body".to_string())
85+
.message_id("msg-456".to_string())
86+
.build();
87+
88+
#[cfg(feature = "catch-all-fields")]
89+
let _sqs_event = SqsEvent::builder()
90+
.records(vec![sqs_message])
91+
.other(serde_json::Map::new())
92+
.build();
93+
94+
#[cfg(not(feature = "catch-all-fields"))]
95+
let _sqs_event = SqsEvent::builder().records(vec![sqs_message]).build();
96+
97+
// Secrets Manager Event - Secret rotation
98+
let _secrets_event = SecretsManagerSecretRotationEvent::builder()
99+
.step("createSecret".to_string())
100+
.secret_id("test-secret".to_string())
101+
.client_request_token("token-123".to_string())
102+
.build();
103+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Example showing how builders work with generic types and custom context structs
2+
//
3+
// Demonstrates:
4+
// 1. Generic types (ApiGatewayV2CustomAuthorizerSimpleResponse<T>)
5+
// 2. Custom context struct WITHOUT Default implementation
6+
// 3. Custom context struct WITH Default implementation
7+
8+
use aws_lambda_events::event::apigw::{
9+
ApiGatewayV2CustomAuthorizerSimpleResponse, ApiGatewayV2CustomAuthorizerV2Request,
10+
};
11+
use lambda_runtime::{Error, LambdaEvent};
12+
use serde::{Deserialize, Serialize};
13+
14+
// Custom context WITHOUT Default - requires builder pattern
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
pub struct ContextWithoutDefault {
17+
pub user_id: String,
18+
pub api_key: String,
19+
pub permissions: Vec<String>,
20+
}
21+
22+
// Custom context WITH Default - works both ways
23+
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
24+
pub struct ContextWithDefault {
25+
pub user_id: String,
26+
pub role: String,
27+
}
28+
29+
// Handler using context WITHOUT Default - builder pattern required
30+
pub async fn handler_without_default(
31+
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
32+
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<ContextWithoutDefault>, Error> {
33+
let context = ContextWithoutDefault {
34+
user_id: "user-123".to_string(),
35+
api_key: "secret-key".to_string(),
36+
permissions: vec!["read".to_string()],
37+
};
38+
39+
let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
40+
.is_authorized(true)
41+
.context(context)
42+
.build();
43+
44+
Ok(response)
45+
}
46+
47+
// Handler using context WITH Default - builder pattern still preferred
48+
pub async fn handler_with_default(
49+
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
50+
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<ContextWithDefault>, Error> {
51+
let context = ContextWithDefault {
52+
user_id: "user-456".to_string(),
53+
role: "admin".to_string(),
54+
};
55+
56+
let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
57+
.is_authorized(true)
58+
.context(context)
59+
.build();
60+
61+
Ok(response)
62+
}
63+
64+
fn main() {
65+
// Example 1: Context WITHOUT Default
66+
let context_no_default = ContextWithoutDefault {
67+
user_id: "user-123".to_string(),
68+
api_key: "secret-key".to_string(),
69+
permissions: vec!["read".to_string(), "write".to_string()],
70+
};
71+
72+
let response1 = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
73+
.is_authorized(true)
74+
.context(context_no_default)
75+
.build();
76+
77+
println!("Response with context WITHOUT Default:");
78+
println!(" User: {}", response1.context.user_id);
79+
println!(" Authorized: {}", response1.is_authorized);
80+
81+
// Example 2: Context WITH Default
82+
let context_with_default = ContextWithDefault {
83+
user_id: "user-456".to_string(),
84+
role: "admin".to_string(),
85+
};
86+
87+
let response2 = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
88+
.is_authorized(false)
89+
.context(context_with_default)
90+
.build();
91+
92+
println!("\nResponse with context WITH Default:");
93+
println!(" User: {}", response2.context.user_id);
94+
println!(" Role: {}", response2.context.role);
95+
println!(" Authorized: {}", response2.is_authorized);
96+
}

0 commit comments

Comments
 (0)