Skip to content
Open
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
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 }
derive_builder = { version = "0.20", optional = true }
Copy link
Collaborator

Choose a reason for hiding this comment

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

How would you feel about using bon instead? The big reason would be that it uses typestate for compile time verification, which is pretty nice for ergonomics.

The better refactoring support probably doesn't affect us since we are anyway all pub so would break semver either way on internal changes.

The function builders we don't need yet, though I wouldn't be surprised if it is useful later. For instance, we have split into separate APIs for the concurrent mode. Future features like that would be nice to add as optional parameters via a builder.

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 = ["derive_builder"]

[package.metadata.docs.rs]
all-features = true
Expand Down
44 changes: 44 additions & 0 deletions lambda-events/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,50 @@ 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. This enables type-safe, immutable construction of event responses without requiring `Default` trait implementations on custom context types.

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::{
ApiGatewayV2CustomAuthorizerSimpleResponseBuilder,
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 = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::default()
.is_authorized(true)
.context(context)
.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.

## 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
38 changes: 38 additions & 0 deletions lambda-events/examples/comprehensive-builders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Example demonstrating builder pattern usage for AWS Lambda events
#[cfg(feature = "builders")]
use aws_lambda_events::event::{
dynamodb::EventBuilder as DynamoDbEventBuilder, kinesis::KinesisEventBuilder, s3::S3EventBuilder,
secretsmanager::SecretsManagerSecretRotationEventBuilder, sns::SnsEventBuilder, sqs::SqsEventBuilder,
};

#[cfg(feature = "builders")]
fn main() {
// S3 Event - Object storage notifications
let s3_event = S3EventBuilder::default().records(vec![]).build().unwrap();

// Kinesis Event - Stream processing
let kinesis_event = KinesisEventBuilder::default().records(vec![]).build().unwrap();

// DynamoDB Event - Database change streams
let dynamodb_event = DynamoDbEventBuilder::default().records(vec![]).build().unwrap();

// SNS Event - Pub/sub messaging
let sns_event = SnsEventBuilder::default().records(vec![]).build().unwrap();

// SQS Event - Queue messaging
let sqs_event = SqsEventBuilder::default().records(vec![]).build().unwrap();

// Secrets Manager Event - Secret rotation
let secrets_event = SecretsManagerSecretRotationEventBuilder::default()
.step("createSecret")
.secret_id("test-secret")
.client_request_token("token-123")
.build()
.unwrap();
}

#[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");
}
148 changes: 148 additions & 0 deletions lambda-events/examples/lambda-runtime-authorizer-builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Example showing how builders solve the Default trait requirement problem
// when using lambda_runtime with API Gateway custom authorizers
//
// ❌ OLD WAY (with Default requirement):
// #[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 = ResponseBuilder::default()
// .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, ApiGatewayV2CustomAuthorizerSimpleResponseBuilder,
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 = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::default()
.is_authorized(true)
.context(context)
.build()
.map_err(|e| format!("Failed to build response: {}", e))?;

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 = ApiGatewayV2CustomAuthorizerSimpleResponseBuilder::<MyContext>::default()
.is_authorized(true)
.context(context)
.build()
.unwrap();

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