Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,80 @@ By default, the log level to emit events is `INFO`. Log at `TRACE` level for mor

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.

### Builder pattern for event responses

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`.

Enable the builders feature in your `Cargo.toml`:

```toml
[dependencies]
aws_lambda_events = { version = "*", features = ["builders"] }
```

Example with API Gateway custom authorizers:

```rust
use aws_lambda_events::event::apigw::{
ApiGatewayV2CustomAuthorizerSimpleResponseBuilder,
ApiGatewayV2CustomAuthorizerV2Request,
};
use lambda_runtime::{Error, LambdaEvent};

struct MyContext {
user_id: String,
permissions: Vec<String>,
}

async fn handler(
event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContext>, Error> {
let context = MyContext {
user_id: "user-123".to_string(),
permissions: vec!["read".to_string()],
};

let response = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::default()
.is_authorized(true)
.context(context)
.build()?;

Ok(response)
}
```

Example with SQS batch responses:

```rust
use aws_lambda_events::event::sqs::{
BatchItemFailureBuilder,
SqsBatchResponseBuilder,
SqsEvent,
};
use lambda_runtime::{Error, LambdaEvent};

async fn handler(event: LambdaEvent<SqsEvent>) -> Result<SqsBatchResponse, Error> {
let mut failures = Vec::new();

for record in event.payload.records {
if let Err(_) = process_record(&record).await {
let failure = BatchItemFailureBuilder::default()
.item_identifier(record.message_id.unwrap())
.build()?;
failures.push(failure);
}
}

let response = SqsBatchResponseBuilder::default()
.batch_item_failures(failures)
.build()?;

Ok(response)
}
```

See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples.

### Custom event objects

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:
Expand Down
2 changes: 2 additions & 0 deletions lambda-events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ edition = "2021"
base64 = { workspace = true }
bytes = { workspace = true, features = ["serde"], optional = true }
chrono = { workspace = true, optional = true }
bon = { version = "3", optional = true }
flate2 = { version = "1.0.24", optional = true }
http = { workspace = true, optional = true }
http-body = { workspace = true, optional = true }
Expand Down Expand Up @@ -126,6 +127,7 @@ documentdb = []
eventbridge = ["chrono", "serde_with"]

catch-all-fields = []
builders = ["bon"]

[package.metadata.docs.rs]
all-features = true
Expand Down
50 changes: 50 additions & 0 deletions lambda-events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,56 @@ This crate divides all Lambda Events into features named after the service that
cargo add aws_lambda_events --no-default-features --features apigw,alb
```

### Builder pattern support

The crate provides an optional `builders` feature that adds builder pattern support for event types using the [bon](https://crates.io/crates/bon) crate. This enables type-safe, immutable construction of event responses with a clean, ergonomic API.

@jlizen jlizen Jan 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: bon feels like an implementation detail. We are not exposing bon specific types in our public API contract and might change what builder we use in the future (while preserving the API). Not sold on mentioning it in the README, is there anything we expect users to need to go and look up in bon docs to understand?


Enable the builders feature:

```
cargo add aws_lambda_events --features builders
```

Example using builders with API Gateway custom authorizers:

```rust
use aws_lambda_events::event::apigw::{
ApiGatewayV2CustomAuthorizerSimpleResponse,
ApiGatewayV2CustomAuthorizerV2Request,
};
use lambda_runtime::{Error, LambdaEvent};

// Context type without Default implementation
struct MyContext {
user_id: String,
permissions: Vec<String>,
}

async fn handler(
event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContext>, Error> {
let context = MyContext {
user_id: "user-123".to_string(),
permissions: vec!["read".to_string()],
};

let response = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
.is_authorized(true)
.context(context)
.build();

Ok(response)
}
```

Key benefits of bon builders:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably ditch this, unless we want to have some sort of INTERNALS.md to explain design decisions. It doesn't feel like it belongs in end-user docs though.

- **Clean API**: `Struct::builder().field().build()` - no `.unwrap()` or `?` needed
- **Automatic Option handling**: Optional fields don't need to be explicitly set
- **Type safety**: Compile-time validation of required fields
- **Ergonomic**: Minimal configuration required

See the [examples directory](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-events/examples) for more builder pattern examples.

## History

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.
Expand Down
44 changes: 44 additions & 0 deletions lambda-events/examples/comprehensive-builders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Example demonstrating builder pattern usage for AWS Lambda events
#[cfg(feature = "builders")]
use aws_lambda_events::event::{
dynamodb::Event as DynamoDbEvent, kinesis::KinesisEvent, s3::S3Event,
secretsmanager::SecretsManagerSecretRotationEvent, sns::SnsEvent, sqs::SqsEvent,
};

#[cfg(feature = "builders")]
fn main() {
// S3 Event - Object storage notifications

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: These would be much more useful examples if they included at least one record.

let _s3_event = S3Event::builder().records(vec![]).build();

// Kinesis Event - Stream processing
let _kinesis_event = KinesisEvent::builder().records(vec![]).build();

// DynamoDB Event - Database change streams
let _dynamodb_event = DynamoDbEvent::builder().records(vec![]).build();

// SNS Event - Pub/sub messaging
let _sns_event = SnsEvent::builder().records(vec![]).build();

// SQS Event - Queue messaging
#[cfg(feature = "catch-all-fields")]
let _sqs_event = SqsEvent::builder()
.records(vec![])
.other(serde_json::Map::new())
.build();

#[cfg(not(feature = "catch-all-fields"))]
let _sqs_event = SqsEvent::builder().records(vec![]).build();

// Secrets Manager Event - Secret rotation
let _secrets_event = SecretsManagerSecretRotationEvent::builder()
.step("createSecret".to_string())
.secret_id("test-secret".to_string())
.client_request_token("token-123".to_string())
.build();
}

#[cfg(not(feature = "builders"))]
fn main() {
println!("This example requires the 'builders' feature to be enabled.");
println!("Run with: cargo run --example comprehensive-builders --all-features");
}
145 changes: 145 additions & 0 deletions lambda-events/examples/lambda-runtime-authorizer-builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Example showing how builders solve the Default trait requirement problem
// when using lambda_runtime with API Gateway custom authorizers
//
// ❌ OLD WAY (with Default requirement):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'm not sure there is much value in showing the old way. It adds noise to this example. Shouldn't readers just be pointed to the recommended pattern without the history lesson?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything, this would be a great code snippet to include in the release notes

// #[derive(Default)]
// struct MyContext {
// // Had to use Option just for Default
// some_thing: Option<ThirdPartyThing>,
// }
//
// let mut output = Response::default();
// output.is_authorized = true;
// output.context = MyContext {
// some_thing: Some(thing), // ❌ Unnecessary Some()
// };
//
// ✅ NEW WAY (with Builder pattern):
// struct MyContext {
// // No Option needed!
// some_thing: ThirdPartyThing,
// }
//
// let output = Response::builder()
// .is_authorized(true)
// .context(context)
// .build()?;
//
// Benefits:
// • No Option<T> wrapper for fields that always exist
// • Type-safe construction
// • Works seamlessly with lambda_runtime::LambdaEvent
// • Cleaner, more idiomatic Rust code

#[cfg(feature = "builders")]
use aws_lambda_events::event::apigw::{
ApiGatewayV2CustomAuthorizerSimpleResponse, ApiGatewayV2CustomAuthorizerV2Request,
};
#[cfg(feature = "builders")]
use lambda_runtime::{Error, LambdaEvent};
#[cfg(feature = "builders")]
use serde::{Deserialize, Serialize};

#[cfg(feature = "builders")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SomeThirdPartyThingWithoutDefaultValue {
pub api_key: String,
pub endpoint: String,
pub timeout_ms: u64,
}

// ❌ OLD WAY: Had to use Option to satisfy Default requirement
#[cfg(feature = "builders")]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct MyContextOldWay {
// NOT IDEAL: Need to wrap with Option just for Default
some_thing_always_exists: Option<SomeThirdPartyThingWithoutDefaultValue>,
}

// ✅ NEW WAY: No Option needed with builder pattern!
#[cfg(feature = "builders")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MyContext {
// IDEAL: Can use the actual type directly!
some_thing_always_exists: SomeThirdPartyThingWithoutDefaultValue,
user_id: String,
permissions: Vec<String>,
}

// ❌ OLD IMPLEMENTATION: Using Default
#[cfg(feature = "builders")]
pub async fn function_handler_old_way(
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContextOldWay>, Error> {
let mut output: ApiGatewayV2CustomAuthorizerSimpleResponse<MyContextOldWay> =
ApiGatewayV2CustomAuthorizerSimpleResponse::default();

output.is_authorized = true;
output.context = MyContextOldWay {
// ❌ Had to wrap in Some() even though it always exists
some_thing_always_exists: Some(SomeThirdPartyThingWithoutDefaultValue {
api_key: "secret-key-123".to_string(),
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
}),
};

Ok(output)
}

// ✅ NEW IMPLEMENTATION: Using Builder
#[cfg(feature = "builders")]
pub async fn function_handler(
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<MyContext>, Error> {
let context = MyContext {
// ✅ No Option wrapper needed!
some_thing_always_exists: SomeThirdPartyThingWithoutDefaultValue {
api_key: "secret-key-123".to_string(),
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
},
user_id: "user-123".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
};

// ✅ Clean builder pattern - no Default required!
let output = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
.is_authorized(true)
.context(context)
.build();

Ok(output)
}

#[cfg(feature = "builders")]
fn main() {
let context = MyContext {
some_thing_always_exists: SomeThirdPartyThingWithoutDefaultValue {
api_key: "secret-key-123".to_string(),
endpoint: "https://api.example.com".to_string(),
timeout_ms: 5000,
},
user_id: "user-123".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
};

let response = ApiGatewayV2CustomAuthorizerSimpleResponse::<MyContext>::builder()
.is_authorized(true)
.context(context)
.build();

println!("✅ Built authorizer response for user: {}", response.context.user_id);
println!(" Authorized: {}", response.is_authorized);
println!(" Permissions: {:?}", response.context.permissions);
println!(
" Third-party endpoint: {}",
response.context.some_thing_always_exists.endpoint
);
}

#[cfg(not(feature = "builders"))]
fn main() {
println!("This example requires the 'builders' feature to be enabled.");
println!("Run with: cargo run --example lambda-runtime-authorizer-builder --features builders");
}
Loading
Loading