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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ members = [
"lambda-events",
]

exclude = ["examples"]
exclude = ["examples","lambda-events/lambda-events-examples"]

[workspace.dependencies]
base64 = "0.22"
Expand Down
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
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 with a clean, ergonomic API.

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)
}
```

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
25 changes: 25 additions & 0 deletions lambda-events/lambda-events-examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "lambda-events-examples"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
aws_lambda_events = { path = "..", features = ["builders"] }
lambda_runtime = { path = "../../lambda-runtime" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", default-features = false, features = ["clock"] }
serde_dynamo = "4"

[[example]]
name = "comprehensive-builders"
path = "examples/comprehensive-builders.rs"

[[example]]
name = "lambda-runtime-authorizer-builder"
path = "examples/lambda-runtime-authorizer-builder.rs"

[[example]]
name = "lambda-runtime-sqs-batch-builder"
path = "examples/lambda-runtime-sqs-batch-builder.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Example demonstrating builder pattern usage for AWS Lambda events
use aws_lambda_events::event::{
dynamodb::{Event as DynamoDbEvent, EventRecord as DynamoDbEventRecord, StreamRecord},
kinesis::{KinesisEvent, KinesisEventRecord, KinesisRecord, KinesisEncryptionType},
s3::{S3Event, S3EventRecord, S3Entity, S3Bucket, S3Object, S3RequestParameters, S3UserIdentity},
secretsmanager::SecretsManagerSecretRotationEvent,
sns::{SnsEvent, SnsRecord, SnsMessage},
sqs::{SqsEvent, SqsMessage},
};
use std::collections::HashMap;

fn main() {
// S3 Event - Object storage notifications with nested structures
let s3_record = S3EventRecord::builder()
.event_time(chrono::Utc::now())
.principal_id(S3UserIdentity::builder().build())
.request_parameters(S3RequestParameters::builder().build())
.response_elements(HashMap::new())
.s3(S3Entity::builder()
.bucket(S3Bucket::builder().name("my-bucket".to_string()).build())
.object(S3Object::builder().key("file.txt".to_string()).size(1024).build())
.build())
.build();
let _s3_event = S3Event::builder().records(vec![s3_record]).build();

// Kinesis Event - Stream processing with data
let kinesis_record = KinesisEventRecord::builder()
.kinesis(KinesisRecord::builder()
.data(serde_json::from_str("\"SGVsbG8gV29ybGQ=\"").unwrap())
.partition_key("key-1".to_string())
.sequence_number("12345".to_string())
.approximate_arrival_timestamp(serde_json::from_str("1234567890.0").unwrap())
.encryption_type(KinesisEncryptionType::None)
.build())
.build();
let _kinesis_event = KinesisEvent::builder().records(vec![kinesis_record]).build();

// DynamoDB Event - Database change streams with item data
let mut keys = HashMap::new();
keys.insert("id".to_string(), serde_dynamo::AttributeValue::S("123".to_string()));

let dynamodb_record = DynamoDbEventRecord::builder()
.aws_region("us-east-1".to_string())
.change(StreamRecord::builder()
.approximate_creation_date_time(chrono::Utc::now())
.keys(keys.into())
.new_image(HashMap::new().into())
.old_image(HashMap::new().into())
.size_bytes(100)
.build())
.event_id("event-123".to_string())
.event_name("INSERT".to_string())
.build();
let _dynamodb_event = DynamoDbEvent::builder().records(vec![dynamodb_record]).build();

// SNS Event - Pub/sub messaging with message details
let sns_record = SnsRecord::builder()
.event_source("aws:sns".to_string())
.event_version("1.0".to_string())
.event_subscription_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string())
.sns(SnsMessage::builder()
.message("Hello from SNS".to_string())
.sns_message_type("Notification".to_string())
.message_id("msg-123".to_string())
.topic_arn("arn:aws:sns:us-east-1:123456789012:topic".to_string())
.timestamp(chrono::Utc::now())
.signature_version("1".to_string())
.signature("sig".to_string())
.signing_cert_url("https://cert.url".to_string())
.unsubscribe_url("https://unsub.url".to_string())
.message_attributes(HashMap::new())
.build())
.build();
let _sns_event = SnsEvent::builder().records(vec![sns_record]).build();

// SQS Event - Queue messaging with attributes
let mut attrs = HashMap::new();
attrs.insert("ApproximateReceiveCount".to_string(), "1".to_string());
attrs.insert("SentTimestamp".to_string(), "1234567890".to_string());

let sqs_message = SqsMessage::builder()
.attributes(attrs)
.message_attributes(HashMap::new())
.body("message body".to_string())
.message_id("msg-456".to_string())
.build();

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

#[cfg(not(feature = "catch-all-fields"))]
let _sqs_event = SqsEvent::builder().records(vec![sqs_message]).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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Example showing how builders work with generic types and custom context structs
//
// Demonstrates:
// 1. Generic types (ApiGatewayV2CustomAuthorizerSimpleResponse<T>)
// 2. Custom context struct WITHOUT Default implementation
// 3. Custom context struct WITH Default implementation

use aws_lambda_events::event::apigw::{
ApiGatewayV2CustomAuthorizerSimpleResponse, ApiGatewayV2CustomAuthorizerV2Request,
};
use lambda_runtime::{Error, LambdaEvent};
use serde::{Deserialize, Serialize};

// Custom context WITHOUT Default - requires builder pattern
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextWithoutDefault {
pub user_id: String,
pub api_key: String,
pub permissions: Vec<String>,
}

// Custom context WITH Default - works both ways
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ContextWithDefault {
pub user_id: String,
pub role: String,
}

// Handler using context WITHOUT Default - builder pattern required
pub async fn handler_without_default(
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<ContextWithoutDefault>, Error> {
let context = ContextWithoutDefault {
user_id: "user-123".to_string(),
api_key: "secret-key".to_string(),
permissions: vec!["read".to_string()],
};

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

Ok(response)
}

// Handler using context WITH Default - builder pattern still preferred
pub async fn handler_with_default(
_event: LambdaEvent<ApiGatewayV2CustomAuthorizerV2Request>,
) -> Result<ApiGatewayV2CustomAuthorizerSimpleResponse<ContextWithDefault>, Error> {
let context = ContextWithDefault {
user_id: "user-456".to_string(),
role: "admin".to_string(),
};

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

Ok(response)
}

fn main() {
// Example 1: Context WITHOUT Default
let context_no_default = ContextWithoutDefault {
user_id: "user-123".to_string(),
api_key: "secret-key".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
};

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

println!("Response with context WITHOUT Default:");
println!(" User: {}", response1.context.user_id);
println!(" Authorized: {}", response1.is_authorized);

// Example 2: Context WITH Default
let context_with_default = ContextWithDefault {
user_id: "user-456".to_string(),
role: "admin".to_string(),
};

let response2 = ApiGatewayV2CustomAuthorizerSimpleResponse::builder()
.is_authorized(false)
.context(context_with_default)
.build();

println!("\nResponse with context WITH Default:");
println!(" User: {}", response2.context.user_id);
println!(" Role: {}", response2.context.role);
println!(" Authorized: {}", response2.is_authorized);
}
Loading
Loading