-
Notifications
You must be signed in to change notification settings - Fork 393
Add builder pattern support for event response types #1090
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
071ff54
06d9bb9
9f87a56
fa2af97
f9fd468
86cd174
09b5819
610b520
bbd6773
6afee4e
f40cfa1
1231e8c
ebd69a6
b10f52b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
||
| 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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
| } | ||
| 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): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
bonfeels 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?