diff --git a/.github/actions/rust-build/action.yml b/.github/actions/rust-build/action.yml index 85b5c0e85..3eb857e9b 100644 --- a/.github/actions/rust-build/action.yml +++ b/.github/actions/rust-build/action.yml @@ -7,6 +7,10 @@ inputs: toolchain: required: true description: "the Rust toolchain to use" + run-tests: + required: true + default: true + description: "whether to run tests in addition to building" runs: using: "composite" @@ -22,5 +26,8 @@ runs: run: cargo build --all-features --verbose --package ${{ inputs.package }} - name: Run tests + if: ${{ inputs.run-tests == 'true' }} shell: bash + env: + RUSTFLAGS: ${{ inputs.package == 'lambda_runtime' && '--cfg tokio_unstable' || '' }} run: cargo test --all-features --verbose --package ${{ inputs.package }} diff --git a/.github/workflows/build-events.yml b/.github/workflows/build-events.yml index 6894e3316..8f0e6d1dc 100644 --- a/.github/workflows/build-events.yml +++ b/.github/workflows/build-events.yml @@ -10,19 +10,21 @@ on: - "lambda-events/**" - "Cargo.toml" +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest strategy: matrix: toolchain: - - "1.71.1" # Current MSRV + - "1.84.0" # Current MSRV - stable env: RUST_BACKTRACE: 1 steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v6 - name: Build events uses: ./.github/actions/rust-build with: @@ -31,9 +33,26 @@ jobs: check-event-features: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Test individual event features run: make check-event-features + semver: + name: semver + needs: [build, check-event-features] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `aws_lambda_events` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: aws_lambda_events + feature-group: default-features + - name: Check `aws_lambda_events` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: aws_lambda_events + feature-group: all-features diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml index cb23c2890..90d5bf593 100644 --- a/.github/workflows/build-extension.yml +++ b/.github/workflows/build-extension.yml @@ -1,4 +1,4 @@ -name: Check Lambda Runtime +name: Check Lambda Extension on: push: @@ -16,28 +16,46 @@ on: - 'Cargo.toml' +permissions: + contents: read + jobs: build-runtime: runs-on: ubuntu-latest strategy: matrix: toolchain: - - "1.71.1" # Current MSRV + - "1.84.0" # Current MSRV - stable env: RUST_BACKTRACE: 1 steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v6 - name: Build Runtime API Client uses: ./.github/actions/rust-build with: package: lambda_runtime_api_client toolchain: ${{ matrix.toolchain}} - - - name: Build Extensions runtime uses: ./.github/actions/rust-build with: package: lambda-extension toolchain: ${{ matrix.toolchain}} + semver: + name: semver + needs: build-runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `lambda-extension` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda-extension + feature-group: default-features + - name: Check `lambda-extension` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda-extension + feature-group: all-features diff --git a/.github/workflows/build-integration-test.yml b/.github/workflows/build-integration-test.yml new file mode 100644 index 000000000..4308c6d62 --- /dev/null +++ b/.github/workflows/build-integration-test.yml @@ -0,0 +1,43 @@ +name: Build integration tests + +on: + push: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'lambda-extension/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime-api-client/**' + - 'lambda-runtime/**' + - 'lambda-http/**' + - 'lambda-extension/**' + - 'Cargo.toml' + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - "1.84.0" # Current MSRV + - stable + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + + - name: Build Integration tests + uses: ./.github/actions/rust-build + with: + package: lambda-integration-tests + toolchain: ${{ matrix.toolchain}} + # the tests will generally fail in ci since they make a network call to a real endpoint, + # this step is just designed to make sure they build successfully + run-tests: false diff --git a/.github/workflows/build-runtime.yml b/.github/workflows/build-runtime.yml index dfc59ee86..16603df2f 100644 --- a/.github/workflows/build-runtime.yml +++ b/.github/workflows/build-runtime.yml @@ -15,33 +15,51 @@ on: - 'lambda-http/**' - 'Cargo.toml' +permissions: + contents: read + jobs: build-runtime: runs-on: ubuntu-latest strategy: matrix: toolchain: - - "1.71.1" # Current MSRV + - "1.84.0" # Current MSRV - stable env: RUST_BACKTRACE: 1 steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v6 - name: Build Runtime API Client uses: ./.github/actions/rust-build with: package: lambda_runtime_api_client toolchain: ${{ matrix.toolchain}} - - name: Build Functions runtime uses: ./.github/actions/rust-build with: package: lambda_runtime toolchain: ${{ matrix.toolchain}} - - name: Build HTTP layer uses: ./.github/actions/rust-build with: package: lambda_http toolchain: ${{ matrix.toolchain}} + semver: + name: semver + needs: build-runtime + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Check `lambda_runtime_api_client`, `lambda_runtime`, lambda_http` semver with only default features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda_runtime_api_client, lambda_runtime, lambda_http + feature-group: default-features + - name: Check `lambda_runtime_api_client`, `lambda_runtime`, lambda_http` semver with all features + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: stable + package: lambda_runtime_api_client, lambda_runtime, lambda_http + feature-group: all-features diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml new file mode 100644 index 000000000..252122b5b --- /dev/null +++ b/.github/workflows/check-docs.yml @@ -0,0 +1,41 @@ +name: Check rustdocs +# this is its own workflow since we to to use unstable +# to have the docs.rs display of feature flags + +on: + push: + paths: + - 'lambda-runtime/**' + - 'lambda-runtime-api-client/**' + - 'lambda-http/**' + - 'lambda-events/**' + - 'lambda-extension/**' + - 'Cargo.toml' + + pull_request: + paths: + - 'lambda-runtime/**' + - 'lambda-runtime-api-client/**' + - 'lambda-http/**' + - 'lambda-events/**' + - 'lambda-extension/**' + - 'Cargo.toml' + +permissions: + contents: read + +jobs: + build-runtime: + runs-on: ubuntu-latest + + env: + RUST_BACKTRACE: 1 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@nightly + + - name: Check documentation + shell: bash + env: + RUSTDOCFLAGS: --cfg docsrs -Dwarnings + run: cargo doc --workspace --no-deps --document-private-items --all-features diff --git a/.github/workflows/check-examples.yml b/.github/workflows/check-examples.yml index 5ef1536ae..cec9ac1e6 100644 --- a/.github/workflows/check-examples.yml +++ b/.github/workflows/check-examples.yml @@ -5,11 +5,14 @@ on: branches: [main] pull_request: +permissions: + contents: read + jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/closed-issue-message.yml b/.github/workflows/closed-issue-message.yml index 2a73fe92d..6fbc007a5 100644 --- a/.github/workflows/closed-issue-message.yml +++ b/.github/workflows/closed-issue-message.yml @@ -1,15 +1,18 @@ -name: Closed Issue Message -on: - issues: - types: [closed] -jobs: - auto_comment: - runs-on: ubuntu-latest - steps: - - uses: aws-actions/closed-issue-message@v1 - with: - # These inputs are both required - repo-token: "${{ secrets.GITHUB_TOKEN }}" - message: | - This issue is now closed. Comments on closed issues are hard for our team to see. - If you need more assistance, please either tag a team member or open a new issue that references this one. +name: Closed Issue Message +on: + issues: + types: [closed] +permissions: + issues: write + +jobs: + auto_comment: + runs-on: ubuntu-latest + steps: + - uses: aws-actions/closed-issue-message@v2 + with: + # These inputs are both required + repo-token: "${{ secrets.GITHUB_TOKEN }}" + message: | + This issue is now closed. Comments on closed issues are hard for our team to see. + If you need more assistance, please either tag a team member or open a new issue that references this one. diff --git a/.github/workflows/dockerized-test.yml b/.github/workflows/dockerized-test.yml new file mode 100644 index 000000000..be86e1470 --- /dev/null +++ b/.github/workflows/dockerized-test.yml @@ -0,0 +1,72 @@ +name: dockerized-test + +permissions: + contents: read + +on: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + workflow_dispatch: + + +jobs: + dockerized-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build Lambda artifacts for testing + run: | + mkdir -p test/dockerized/tasks + HANDLERS_TO_BUILD="basic-lambda basic-sqs http-basic-lambda basic-lambda-concurrent" OUTPUT_DIR="$(pwd)/test/dockerized/tasks" ./scripts/build-examples.sh + ls -la test/dockerized/tasks/ + + - name: Build base test image with RIE and custom entrypoint + run: | + docker build . -t local/test-base -f Dockerfile.test + + - name: Run tests + uses: aws/containerized-test-runner-for-aws-lambda@main + with: + suiteFileArray: '["./test/dockerized/suites/*.json"]' + dockerImageName: 'local/test-base' + taskFolder: './test/dockerized/tasks' + + dockerized-test-concurrent: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + + - name: Build Lambda artifacts for testing + run: | + mkdir -p test/dockerized/tasks + HANDLERS_TO_BUILD="basic-lambda-concurrent invocation-id-concurrent" OUTPUT_DIR="$(pwd)/test/dockerized/tasks" ./scripts/build-examples.sh + ls -la test/dockerized/tasks/ + + - name: Build base test image with RIE and custom entrypoint + run: docker build -t local/test-base -f Dockerfile.test . + + - name: Create Docker network for concurrent tests + run: docker network create concurrent-test-net + + - name: Run concurrent scenarios + uses: aws/containerized-test-runner-for-aws-lambda@main + env: + CONTAINER_READY_DELAY_SECS: 5 + with: + suiteFileArray: '[]' + dockerImageName: 'local/test-base' + taskFolder: './test/dockerized/tasks' + scenarioDir: './test/dockerized/scenarios' + dockerSharedNetwork: 'concurrent-test-net' + + - name: Remove Docker network + if: always() + run: docker network rm concurrent-test-net || true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 10f8c75f2..507b3dccf 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -2,12 +2,15 @@ name: Formatting and Linting on: [push, pull_request] +permissions: + contents: read + jobs: fmt: name: Cargo fmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt @@ -20,7 +23,7 @@ jobs: name: Cargo clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run clippy check diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 000000000..5bb826495 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,54 @@ +name: Publish release + +on: + push: + branches: + - main + +jobs: + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'aws' }} + permissions: + contents: write + id-token: write + steps: + - &checkout + name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - &install-rust + name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Authenticate to crates.io + uses: rust-lang/crates-io-auth-action@v1 + id: auth + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} + + release-plz-pr: + name: Release-plz PR + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'aws' }} + permissions: + pull-requests: write + contents: write + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - *checkout + - *install-rust + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }} diff --git a/.github/workflows/run-integration-test.yml b/.github/workflows/run-integration-test.yml index f1e30d097..d43ed9c71 100644 --- a/.github/workflows/run-integration-test.yml +++ b/.github/workflows/run-integration-test.yml @@ -10,25 +10,30 @@ on: jobs: run-integration-tests: + # Only run on the main repo, not forks + if: ${{ github.repository_owner == 'aws' }} runs-on: ubuntu-latest + concurrency: + group: integration-test + cancel-in-progress: false steps: - name: install Cargo Lambda - uses: jaxxstorm/action-install-gh-release@v1.9.0 + uses: jaxxstorm/action-install-gh-release@v2.1.0 with: repo: cargo-lambda/cargo-lambda platform: linux arch: x86_64 - name: install Zig toolchain - uses: korandoru/setup-zig@v1 + uses: mlugg/setup-zig@v2 with: - zig-version: 0.10.0 + version: 0.10.0 - name: install SAM uses: aws-actions/setup-sam@v2 with: use-installer: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v4.0.2 + uses: aws-actions/configure-aws-credentials@v6.0.0 with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} role-session-name: ${{ secrets.ROLE_SESSION_NAME }} @@ -47,15 +52,18 @@ jobs: echo "STACK_NAME=$stackName" >> "$GITHUB_OUTPUT" echo "Stack name = $stackName" sam deploy --stack-name "${stackName}" --parameter-overrides "ParameterKey=SecretToken,ParameterValue=${{ secrets.SECRET_TOKEN }}" "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE }}" --no-confirm-changeset --no-progressbar > disable_output - TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | .OutputValue') + TEST_ENDPOINT=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="HelloApiEndpoint") | .OutputValue') + TENANT_ID_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="TenantIdTestFunction") | .OutputValue') echo "TEST_ENDPOINT=$TEST_ENDPOINT" >> "$GITHUB_OUTPUT" + echo "TENANT_ID_TEST_FUNCTION=$TENANT_ID_TEST_FUNCTION" >> "$GITHUB_OUTPUT" - name: run test env: SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }} TEST_ENDPOINT: ${{ steps.deploy_stack.outputs.TEST_ENDPOINT }} + TENANT_ID_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.TENANT_ID_TEST_FUNCTION }} run: cd lambda-integration-tests && cargo test - name: cleanup - if: always() + if: always() && steps.deploy_stack.outputs.STACK_NAME env: AWS_REGION: ${{ secrets.AWS_REGION }} STACK_NAME: ${{ steps.deploy_stack.outputs.STACK_NAME }} diff --git a/.github/workflows/test-rie.yml b/.github/workflows/test-rie.yml new file mode 100644 index 000000000..2f3dc9f69 --- /dev/null +++ b/.github/workflows/test-rie.yml @@ -0,0 +1,53 @@ +name: Test with RIE + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +permissions: + contents: read + +jobs: + test-rie: + runs-on: ubuntu-latest + strategy: + matrix: + example: [basic-lambda, basic-sqs] + + steps: + - uses: actions/checkout@v6 + + - name: Build and test ${{ matrix.example }} with RIE + run: | + docker build -f Dockerfile.rie --build-arg EXAMPLE=${{ matrix.example }} -t rust-lambda-rie-test-${{ matrix.example }} . + + # Start container in background + docker run -d -p 9000:8080 --name rie-test-${{ matrix.example }} rust-lambda-rie-test-${{ matrix.example }} + + # Wait for container to be ready + sleep 5 + + # Test the function based on example type + if [ "${{ matrix.example }}" = "basic-lambda" ]; then + PAYLOAD='{"command": "test from CI"}' + elif [ "${{ matrix.example }}" = "basic-sqs" ]; then + PAYLOAD='{"Records": [{"body": "{\"id\": \"123\", \"text\": \"hello from SQS\"}", "messageId": "test-id", "receiptHandle": "test-handle", "attributes": {}, "messageAttributes": {}, "md5OfBody": "test-md5", "eventSource": "aws:sqs", "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:test-queue", "awsRegion": "us-east-1"}]}' + fi + + # Make request and verify response + RESPONSE=$(curl -s -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' \ + -d "$PAYLOAD" \ + -H 'Content-Type: application/json') + + echo "Response: $RESPONSE" + + # Basic validation that we got a response (not empty) + if [ -z "$RESPONSE" ]; then + echo "Error: Empty response" + exit 1 + fi + + # Stop container + docker stop rie-test-${{ matrix.example }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index d5e188a43..4fb609825 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,51 @@ -target +# Rust +target/ +**/*.rs.bk +*.pdb + +# Cargo /.cargo -lambda-runtime/libtest.rmeta -lambda-integration-tests/target Cargo.lock -# Built AWS Lambda zipfile -lambda.zip +# IDE +.vscode/ +.idea/ +*.iml +*.ipr +*.iws +.kiro/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Environment +*.env.local -# output.json from example docs +# AWS / SAM +.aws-sam/ +build/ output.json +lambda.zip + +# CDK +node_modules/ +cdk.out/ -.aws-sam -build -.vscode +# Test artifacts +.test-runner/ +test/dockerized/tasks/ +Dockerfile.test-with-tasks +.test/ -node_modules -cdk.out +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.pytest_cache/ + +# Misc +lambda-runtime/libtest.rmeta +lambda-integration-tests/target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b87d31fc..108db9c92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ information to effectively respond to your bug report or contribution. We welcome you to use the GitHub issue tracker to report bugs or suggest features. -When filing an issue, please check [existing open](https://github.com/awslabs/aws-lambda-rust-runtime/issues), or [recently closed](https://github.com/awslabs/aws-lambda-rust-runtime/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +When filing an issue, please check [existing open](https://github.com/aws/aws-lambda-rust-runtime/issues), or [recently closed](https://github.com/aws/aws-lambda-rust-runtime/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps @@ -40,7 +40,7 @@ GitHub provides additional document on [forking a repository](https://help.githu ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-lambda-rust-runtime/labels/help%20wanted) issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-lambda-rust-runtime/labels/help%20wanted) issues is a great place to start. ## Code of Conduct @@ -54,6 +54,6 @@ If you discover a potential security issue in this project we ask that you notif ## Licensing -See the [LICENSE](https://github.com/awslabs/aws-lambda-rust-runtime/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. +See the [LICENSE](https://github.com/aws/aws-lambda-rust-runtime/blob/main/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/Cargo.toml b/Cargo.toml index 867e9c0db..ce1f66402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -resolver = "2" +resolver = "3" members = [ "lambda-http", "lambda-integration-tests", @@ -9,7 +9,7 @@ members = [ "lambda-events", ] -exclude = ["examples"] +exclude = ["examples","lambda-events/lambda-events-examples"] [workspace.dependencies] base64 = "0.22" @@ -32,3 +32,5 @@ pin-project-lite = "0.2" tower = "0.5" tower-layer = "0.3" tower-service = "0.3" + + diff --git a/Dockerfile.rie b/Dockerfile.rie new file mode 100644 index 000000000..b55545b22 --- /dev/null +++ b/Dockerfile.rie @@ -0,0 +1,37 @@ +FROM public.ecr.aws/lambda/provided:al2023 + +RUN dnf install -y gcc +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +ARG TARGETARCH +ENV RIE_VERSION=1.36 \ + RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \ + RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \ + RIE_PATH=/usr/local/bin/aws-lambda-rie + +COPY scripts/download-rie.sh /tmp/download-rie.sh +RUN sh /tmp/download-rie.sh \ + "${TARGETARCH}" \ + "${RIE_VERSION}" \ + "${RIE_SHA256_AMD64}" \ + "${RIE_SHA256_ARM64}" \ + "${RIE_PATH}" \ + && rm /tmp/download-rie.sh + +ARG EXAMPLE=basic-lambda + +COPY Cargo.* /build/ +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY lambda-events /build/lambda-events +COPY lambda-http /build/lambda-http +COPY lambda-extension /build/lambda-extension +COPY examples/${EXAMPLE} /build/examples/${EXAMPLE} + +WORKDIR /build/examples/${EXAMPLE} +RUN cargo build --release +RUN cp target/release/${EXAMPLE} ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [] +CMD [ "/usr/local/bin/aws-lambda-rie", "/var/runtime/bootstrap" ] \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 000000000..ece55bf63 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,22 @@ +FROM public.ecr.aws/lambda/provided:al2023 + +ARG TARGETARCH +ENV RIE_VERSION=1.36 \ + RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \ + RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \ + RIE_PATH=/usr/local/bin/aws-lambda-rie + +COPY scripts/download-rie.sh /tmp/download-rie.sh +RUN sh /tmp/download-rie.sh \ + "${TARGETARCH}" \ + "${RIE_VERSION}" \ + "${RIE_SHA256_AMD64}" \ + "${RIE_SHA256_ARM64}" \ + "${RIE_PATH}" \ + && rm /tmp/download-rie.sh + +COPY scripts/custom-lambda-entrypoint.sh /usr/local/bin/lambda-entrypoint +RUN chmod +x /usr/local/bin/lambda-entrypoint + +ENTRYPOINT ["/usr/local/bin/lambda-entrypoint"] +CMD ["basic-lambda"] diff --git a/Makefile b/Makefile index ecfd76231..ccc415a72 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,19 @@ INTEG_API_INVOKE := RestApiUrl HttpApiUrl INTEG_EXTENSIONS := extension-fn extension-trait logs-trait # Using musl to run extensions on both AL1 and AL2 INTEG_ARCH := x86_64-unknown-linux-musl +RIE_MAX_CONCURRENCY ?= 4 +TEST_RUNNER_BRANCH ?= main +CONTAINER_READY_DELAY_SECS ?= 5 +OUTPUT_DIR ?= test/dockerized/tasks +HANDLERS_TO_BUILD ?= basic-lambda basic-sqs http-basic-lambda basic-lambda-concurrent +HANDLER ?= basic-lambda + +.PHONY: help pr-check integration-tests check-event-features fmt build-examples build-test-runner test-rie test-rie-lmi nuke test-dockerized test-dockerized-concurrent + +.DEFAULT_GOAL := help define uppercase -$(shell sed -r 's/(^|-)(\w)/\U\2/g' <<< $(1)) +$(shell echo '$(1)' | sed -r 's/(^|-)(\w)/\U\2/g') endef pr-check: @@ -106,6 +116,91 @@ check-event-features: cargo test --package aws_lambda_events --no-default-features --features sns cargo test --package aws_lambda_events --no-default-features --features sqs cargo test --package aws_lambda_events --no-default-features --features streams + cargo test --package aws_lambda_events --no-default-features --features vpc_lattice fmt: - cargo +nightly fmt --all \ No newline at end of file + cargo +nightly fmt --all + +build-examples: + HANDLERS_TO_BUILD="$(subst ",,$(HANDLERS_TO_BUILD))" OUTPUT_DIR="$(OUTPUT_DIR)" ./scripts/build-examples.sh + +nuke: + docker kill $$(docker ps -q) + +build-test-runner: build-examples + @echo "Building base Docker image with RIE and custom entrypoint..." + docker build \ + -t local/test-base \ + -f Dockerfile.test \ + . + + @echo "Setting up containerized test runner..." + @if [ ! -d ".test-runner" ]; then \ + echo "Cloning containerized-test-runner-for-aws-lambda..."; \ + git clone --quiet --branch $(TEST_RUNNER_BRANCH) https://github.com/aws/containerized-test-runner-for-aws-lambda.git .test-runner; \ + fi + @echo "Building test runner Docker image..." + @docker build -t test-runner:local -f .test-runner/Dockerfile .test-runner + +test-dockerized-concurrent: HANDLERS_TO_BUILD := basic-lambda-concurrent invocation-id-concurrent +test-dockerized-concurrent: build-test-runner + @echo "Running concurrent scenarios in Docker..." + @docker network rm concurrent-test-net 2>/dev/null || true + @docker network create concurrent-test-net + @docker run --rm \ + -e INPUT_SUITE_FILE_ARRAY='[]' \ + -e INPUT_SCENARIO_DIR=/workspace/test/dockerized/scenarios \ + -e DOCKER_IMAGE_NAME=local/test-base \ + -e TASK_FOLDER=./test/dockerized/tasks \ + -e GITHUB_WORKSPACE=/workspace \ + -e DOCKER_SHARED_NETWORK=concurrent-test-net \ + -e CONTAINER_READY_DELAY_SECS=$(CONTAINER_READY_DELAY_SECS) \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(CURDIR):/workspace" \ + -w /workspace \ + test-runner:local + @docker network rm --force concurrent-test-net 2>/dev/null || true + +test-dockerized: build-test-runner + @echo "Running tests in Docker..." + @docker run --rm \ + -e INPUT_SUITE_FILE_ARRAY='["./test/dockerized/suites/*.json"]' \ + -e DOCKER_IMAGE_NAME=local/test-base \ + -e TASK_FOLDER=./test/dockerized/tasks \ + -e GITHUB_WORKSPACE=/workspace \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$(CURDIR):/workspace" \ + -w /workspace \ + test-runner:local + +test-rie: + HANDLER="$(HANDLER)" ./scripts/test-rie.sh + +# Run RIE in Lambda Managed Instance (LMI) mode with concurrent polling. +test-rie-lmi: + RIE_MAX_CONCURRENCY=$(RIE_MAX_CONCURRENCY) HANDLER="$(HANDLER)" ./scripts/test-rie.sh $(EXAMPLE) + +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Available targets:' + @echo ' pr-check Run pre-commit checks (fmt, clippy, tests)' + @echo ' integration-tests Build and run AWS integration tests' + @echo ' check-event-features Test individual event features' + @echo ' fmt Format code with cargo fmt' + @echo ' build-examples Build example Lambda functions' + @echo ' Usage: EXAMPLES="basic-lambda" OUTPUT_DIR=/ make build-examples' + @echo ' test-rie Test Lambda with Runtime Interface Emulator' + @echo ' Usage: HANDLERS_TO_BUILD="basic-lambda basic-sqs" make test-rie' + @echo ' Usage: HANDLERS_TO_BUILD="basic-lambda" HANDLER="basic-lambda" make test-rie' + @echo ' test-rie-lmi Test RIE in Lambda Managed Instance mode' + @echo ' Usage: RIE_MAX_CONCURRENCY=4 HANDLERS_TO_BUILD="basic-lambda-concurrent" make test-rie-lmi' + @echo ' test-dockerized Run dockerized test harness' + @echo ' test-dockerized-concurrent Run concurrent LMI test scenarios' + @echo ' nuke Kill all running Docker containers' + @echo '' + @echo 'Environment variables:' + @echo ' HANDLERS_TO_BUILD Space-separated list of handlers to build for RIE (for test-rie)' + @echo ' HANDLER Specific handler to run (defaults to first in HANDLERS_TO_BUILD)' + @echo ' OUTPUT_DIR Directory for built binaries (default: test/dockerized/tasks)' + @echo ' RIE_MAX_CONCURRENCY Max concurrent Lambda invocations for LMI mode (for test-rie-lmi)' diff --git a/README.md b/README.md index 1a93d85c6..7b0cc9438 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # Rust Runtime for AWS Lambda -[![Build Status](https://github.com/awslabs/aws-lambda-rust-runtime/workflows/Rust/badge.svg)](https://github.com/awslabs/aws-lambda-rust-runtime/actions) +[![Build Status](https://github.com/aws/aws-lambda-rust-runtime/actions/workflows/check-examples.yml/badge.svg)](https://github.com/aws/aws-lambda-rust-runtime/actions) This package makes it easy to run AWS Lambda Functions written in Rust. This workspace includes multiple crates: - [![Docs](https://docs.rs/lambda_runtime/badge.svg)](https://docs.rs/lambda_runtime) **`lambda-runtime`** is a library that provides a Lambda runtime for applications written in Rust. -- [![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) **`lambda-http`** is a library that makes it easy to write API Gateway proxy event focused Lambda functions in Rust. +- [![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) **`lambda-http`** is a library for writing HTTP Lambda functions in Rust, with support for upstream API Gateway, ALB, Lambda Function URLs, and VPC Lattice. - [![Docs](https://docs.rs/lambda-extension/badge.svg)](https://docs.rs/lambda-extension) **`lambda-extension`** is a library that makes it easy to write Lambda Runtime Extensions in Rust. - [![Docs](https://docs.rs/aws_lambda_events/badge.svg)](https://docs.rs/aws_lambda_events) **`lambda-events`** is a library with strongly-typed Lambda event structs in Rust. - [![Docs](https://docs.rs/lambda_runtime_api_client/badge.svg)](https://docs.rs/lambda_runtime_api_client) **`lambda-runtime-api-client`** is a shared library between the lambda runtime and lambda extension libraries that includes a common API client to talk with the AWS Lambda Runtime API. -The Rust runtime client is an experimental package. It is subject to change and intended only for evaluation purposes. - ## Getting started The easiest way to start writing Lambda functions with Rust is by using [Cargo Lambda](https://www.cargo-lambda.info/), a related project. Cargo Lambda is a Cargo plugin, or subcommand, that provides several commands to help you in your journey with Rust on AWS Lambda. @@ -37,6 +35,11 @@ Or PiP on any system with Python 3 installed: ```bash pip3 install cargo-lambda ``` +Alternatively, you can install the pip package as an executable using [uv](https://docs.astral.sh/uv/) + +```bash +uv tool install cargo-lambda +``` See other installation options in [the Cargo Lambda documentation](https://www.cargo-lambda.info/guide/installation.html). @@ -82,7 +85,7 @@ when a function invocation fails, AWS Lambda expects you to return an object tha } ``` -The Rust Runtime for Lambda uses a struct called `Diagnostic` to represent function errors internally. The runtime implements the converstion of several general errors types, like `std::error::Error`, into `Diagnostic`. For these general implementations, the `error_type` is the name of the value type returned by your function. For example, if your function returns `lambda_runtime::Error`, the `error_type` will be something like `alloc::boxed::Box`, which is not very descriptive. +The Rust Runtime for Lambda uses a struct called `Diagnostic` to represent function errors internally. The runtime implements the conversion of several general error types, like `std::error::Error`, into `Diagnostic`. For these general implementations, the `error_type` is the name of the value type returned by your function. For example, if your function returns `lambda_runtime::Error`, the `error_type` will be something like `alloc::boxed::Box`, which is not very descriptive. ### Implement your own Diagnostic @@ -108,7 +111,7 @@ async fn handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> { } ``` -We recommend you to use the [thiserror crate](https://crates.io/crates/thiserror) to declare your errors. You can see an example on how to integrate `thiserror` with the Runtime's diagnostics in our [example repository](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples/basic-error-thiserror) +We recommend you to use the [thiserror crate](https://crates.io/crates/thiserror) to declare your errors. You can see an example on how to integrate `thiserror` with the Runtime's diagnostics in our [example repository](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-error-thiserror) ### Anyhow, Eyre, and Miette @@ -124,7 +127,53 @@ async fn handler(_event: LambdaEvent) -> Result<(), Diagnostic> { } ``` -You can see more examples on how to use these error crates in our [example repository](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples/basic-error-error-crates-integration). +You can see more examples on how to use these error crates in our [example repository](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-error-error-crates-integration). + +### Graceful shutdown + +`lambda_runtime` offers a helper to simplify configuring graceful shutdown signal handling, `spawn_graceful_shutdown_handler()`. This requires the `graceful-shutdown` feature flag and only supports Unix systems. + +You can use it by passing a `FnOnce` closure that returns an async block. That async block will be executed +when the function receives a `SIGTERM` or `SIGKILL`. + +Note that this helper is opinionated in a number of ways. Most notably: +1. It spawns a task to drive your signal handlers +2. It registers a 'no-op' extension in order to enable graceful shutdown signals +3. It panics on unrecoverable errors + +If you prefer to fine-tune the behavior, refer to the implementation of `spawn_graceful_shutdown_handler()` as a starting point for your own. + +For more information on graceful shutdown handling in AWS Lambda, see: [aws-samples/graceful-shutdown-with-aws-lambda](https://github.com/aws-samples/graceful-shutdown-with-aws-lambda). + +Complete example (cleaning up a non-blocking tracing writer): + +```rust,no_run +use lambda_runtime::{service_fn, LambdaEvent, Error}; +use serde_json::{json, Value}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let func = service_fn(func); + + let (writer, log_guard) = tracing_appender::non_blocking(std::io::stdout()); + lambda_runtime::tracing::init_default_subscriber_with_writer(writer); + + let shutdown_hook = || async move { + std::mem::drop(log_guard); + }; + lambda_runtime::spawn_graceful_shutdown_handler(shutdown_hook).await; + + lambda_runtime::run(func).await?; + Ok(()) +} + +async fn func(event: LambdaEvent) -> Result { + let (event, _context) = event.into_parts(); + let first_name = event["firstName"].as_str().unwrap_or("world"); + + Ok(json!({ "message": format!("Hello, {}!", first_name) })) +} +``` ## Building and deploying your Lambda functions @@ -136,6 +185,21 @@ cargo lambda build --release There are other ways of building your function: manually with the AWS CLI, with [AWS SAM](https://github.com/aws/aws-sam-cli), and with the [Serverless framework](https://serverless.com/framework/). +### CPU jitter entropy and cold starts + +The AWS SDK for Rust commonly uses AWS-LC as its TLS provider. Starting with AWS-LC 1.60 (`aws-lc-rs` 1.14.1), AWS-LC collects CPU jitter as an additional entropy source. This happens once per process and can add anywhere from several milliseconds to about a second of cold-start latency. The effect is most noticeable on smaller functions. + +If that tradeoff does not suit your workload, you can disable CPU jitter entropy at build time in `.cargo/config.toml`: + +```toml +[env] +AWS_LC_SYS_NO_JITTER_ENTROPY = "1" +``` + +The setting requires `aws-lc-sys` 0.32.3 or newer and must be present when the function is compiled. It does not change an already-built binary. AWS-LC will continue to use traditional entropy sources such as `RD_RAND` or `/dev/urandom`, but disabling jitter reduces the number of entropy sources. Consider the security and cold-start requirements of your application before opting out. + +See the [AWS SDK announcement](https://github.com/smithy-lang/smithy-rs/discussions/4541) and the [AWS-LC entropy configuration documentation](https://aws.github.io/aws-lc-rs/resources.html#entropy-configuration) for more detail. The setting applies to `aws-lc-sys`, not `aws-lc-fips-sys`. + ### 1. Cross-compiling your Lambda functions By default, Cargo Lambda builds your functions to run on x86_64 architectures. If you'd like to use a different architecture, use the options described below. @@ -319,7 +383,7 @@ Now you can use the `cargo lambda invoke` to send requests to your function. For cargo lambda invoke --data-ascii '{ "command": "hi" }' ``` -Running the command on a HTTP function (Function URL, API Gateway, etc) will require you to use the appropriate scheme. You can find examples of these schemes [here](https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/lambda-http/tests/data). Otherwise, you will be presented with the following error. +Running the command on a HTTP function (Function URL, API Gateway, etc) will require you to use the appropriate scheme. You can find examples of these schemes [here](https://github.com/aws/aws-lambda-rust-runtime/tree/main/lambda-http/tests/data). Otherwise, you will be presented with the following error. ```rust,no_run Error: serde_json::error::Error @@ -341,6 +405,63 @@ curl -v -X POST \ You can read more about how [cargo lambda watch](https://www.cargo-lambda.info/commands/watch.html) and [cargo lambda invoke](https://www.cargo-lambda.info/commands/invoke.html) work on the project's [documentation page](https://www.cargo-lambda.info). +### Local testing with Runtime Interface Emulator (RIE) + +For testing with the official AWS Lambda Runtime Interface Emulator: + +```bash +make test-rie +``` + +By default, this builds and tests the `basic-lambda` example. To build and test a custom handler: + +```bash +HANDLER="basic-tenant-id" make test-rie +``` + +To test Lambda Managed Instances (concurrent polling): + +```bash +RIE_MAX_CONCURRENCY=4 make test-rie +``` + +Different examples expect different payload formats. Check the example's source code in `examples/EXAMPLE_NAME/src/main.rs` + +### Dockerized test harness + +For automated testing with AWS's containerized test runner: + +```bash +make test-dockerized +``` + +This runs test suites defined in `test/dockerized/*.json` files using the [containerized-test-runner-for-aws-lambda](https://github.com/aws/containerized-test-runner-for-aws-lambda). Test suites specify handlers to test (from examples), request payloads, and expected response assertions. + +Example test suite (`test/dockerized/core.json`): +```json +{ + "tests": [ + { + "name": "test_echo", + "handler": "basic-lambda", + "request": { + "command": "test" + }, + "assertions": [ + { + "response": { + "msg": "Command test executed." + }, + "transform": "{msg: .msg}" + } + ] + } + ] +} +``` + +The `transform` field uses jq syntax to extract specific fields from responses before comparison, useful when responses include dynamic fields like request IDs. + ### Lambda Debug Proxy Lambdas can be run and debugged locally using a special [Lambda debug proxy](https://github.com/rimutaka/lambda-debug-proxy) (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your Lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The Lambda handler code does not need to be modified between the local and AWS versions. @@ -369,47 +490,118 @@ 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, +} + +async fn handler( + event: LambdaEvent, +) -> Result, 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) -> Result { + 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: ```rust,no_run -use serde::{Serialize, Deserialize}; -use serde_json::json; -use std::error::Error; +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct NewIceCreamEvent { - pub flavors: Vec, + pub flavors: Vec, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize)] pub struct NewIceCreamResponse { - pub flavors_added_count: usize, + pub flavors_added_count: usize, } -fn main() -> Result<(), Box> { - let flavors = json!({ - "flavors": [ - "Nocciola", - "抹茶", - "आम" - ] - }); - - let event: NewIceCreamEvent = serde_json::from_value(flavors)?; - let response = NewIceCreamResponse { - flavors_added_count: event.flavors.len(), - }; - serde_json::to_string(&response)?; - +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_runtime::run(service_fn(handler)).await?; Ok(()) } + +async fn handler( + event: LambdaEvent, +) -> Result { + println!("Received ice cream event: {:?}", event.payload); + + Ok(NewIceCreamResponse { + flavors_added_count: event.payload.flavors.len(), + }) +} ``` ## Supported Rust Versions (MSRV) -The AWS Lambda Rust Runtime requires a minimum of Rust 1.71.1, and is not guaranteed to build on compiler versions earlier than that. +The AWS Lambda Rust Runtime requires a minimum of Rust 1.84.0, and is not guaranteed to build on compiler versions earlier than that. ## Security diff --git a/examples/advanced-appconfig-feature-flags/Cargo.toml b/examples/advanced-appconfig-feature-flags/Cargo.toml index 52ebb843f..51b708ecc 100644 --- a/examples/advanced-appconfig-feature-flags/Cargo.toml +++ b/examples/advanced-appconfig-feature-flags/Cargo.toml @@ -15,10 +15,10 @@ edition = "2021" # and it will keep the alphabetic ordering for you. [dependencies] -async-trait = "0.1.68" -lambda_runtime = "0.13" -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +async-trait = "0.1.88" +lambda_runtime = { path = "../../lambda-runtime" } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -thiserror = "1.0" +thiserror = "2.0" tokio = { version = "1", features = ["macros"] } diff --git a/examples/advanced-appconfig-feature-flags/cdk/package-lock.json b/examples/advanced-appconfig-feature-flags/cdk/package-lock.json index 61c9a537b..ae4b827d4 100644 --- a/examples/advanced-appconfig-feature-flags/cdk/package-lock.json +++ b/examples/advanced-appconfig-feature-flags/cdk/package-lock.json @@ -8,8 +8,8 @@ "name": "cdk", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "2.159.1", - "cargo-lambda-cdk": "^0.0.22", + "aws-cdk-lib": "2.253.0", + "cargo-lambda-cdk": "^0.0.35", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, @@ -40,34 +40,29 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.203", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.203.tgz", - "integrity": "sha512-7ZhjD0L62dhWL0yzoLCxvJTU3tLHTz/yEg6GKt3foSj+ljVR1KSP8MuAi+QPb4pT7ZTfVzELMtI36Y/ROuL3ig==" - }, - "node_modules/@aws-cdk/asset-kubectl-v20": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", - "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "36.3.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-36.3.0.tgz", - "integrity": "sha512-mLSYgcMFTNCXrGAD7xob95p9s47/7WwEWUJiexxM46H2GxiijhlhLQJs31AS5uRRP6Cx1DLEu4qayKAUOOVGrw==", + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", "bundleDependencies": [ "jsonschema", "semver" ], "dependencies": { - "jsonschema": "^1.4.1", - "semver": "^7.6.3" + "jsonschema": "~1.4.1", + "semver": "^7.7.4" }, "engines": { - "node": ">= 18.18.0" + "node": ">= 18.0.0" } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { @@ -79,7 +74,7 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.4", "inBundle": true, "license": "ISC", "bin": { @@ -1288,11 +1283,12 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.159.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.159.1.tgz", - "integrity": "sha512-zcOyAs3+DTu+CtLehdOgvyosZ7nbLZ+OfBE6uVNMshXm957oXJrLsu6hehLt81TDxfItWYNluFcXkwepZDm6Ng==", + "version": "2.253.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.0.tgz", + "integrity": "sha512-36nNLOz0WJDWraEzU7zlmYY8nsM+YEroBWpRZ84LZPaeEe4Ozy4RhBvdCz/jIGYtLbtiFnfk+przNW2NNRjz5Q==", "bundleDependencies": [ "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", "case", "fs-extra", "ignore", @@ -1305,27 +1301,46 @@ "mime-types" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.3", "ignore": "^5.3.2", - "jsonschema": "^1.4.1", + "jsonschema": "^1.5.0", "mime-types": "^2.1.35", - "minimatch": "^3.1.2", + "minimatch": "^10.2.3", "punycode": "^2.3.1", - "semver": "^7.6.3", - "table": "^6.8.2", - "yaml": "1.10.2" + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 20.0.0" }, "peerDependencies": { - "constructs": "^10.0.0" + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" } }, "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { @@ -1334,7 +1349,7 @@ "license": "Apache-2.0" }, "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.17.1", + "version": "8.18.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -1379,17 +1394,22 @@ } }, "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", + "version": "4.0.4", "inBundle": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "5.0.5", "inBundle": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/aws-cdk-lib/node_modules/case": { @@ -1416,11 +1436,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/emoji-regex": { "version": "8.0.0", "inBundle": true, @@ -1432,12 +1447,22 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.0.1", + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "inBundle": true, - "license": "MIT" + "license": "BSD-3-Clause" }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.2.0", + "version": "11.3.3", "inBundle": true, "license": "MIT", "dependencies": { @@ -1476,7 +1501,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", + "version": "6.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -1487,7 +1512,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "inBundle": true, "license": "MIT", "engines": { @@ -1519,14 +1544,17 @@ } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", + "version": "10.2.5", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/aws-cdk-lib/node_modules/punycode": { @@ -1546,7 +1574,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.6.3", + "version": "7.7.4", "inBundle": true, "license": "ISC", "bin": { @@ -1597,7 +1625,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.2", + "version": "6.9.0", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -1619,15 +1647,8 @@ "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", "inBundle": true, "license": "ISC", "engines": { @@ -1751,9 +1772,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -1869,22 +1890,22 @@ ] }, "node_modules/cargo-lambda-cdk": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.22.tgz", - "integrity": "sha512-lpRh4TFR03FSWw/Ji6D3ZD18mAJjeWvU8ST5UdS6RwbdSXQT/0TbEjG/ccMCpqNGtrlx+txQ8WYURgtNbnDKcw==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", "bundleDependencies": [ "js-toml" ], "dependencies": { - "js-toml": "^0.1.1" + "js-toml": "^1.0.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.63.0", + "aws-cdk-lib": "^2.215.0", "constructs": "^10.0.5" } }, "node_modules/cargo-lambda-cdk/node_modules/@babel/runtime-corejs3": { - "version": "7.23.2", + "version": "7.26.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -1896,49 +1917,54 @@ } }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/gast": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/types": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0" }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/utils": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0" }, "node_modules/cargo-lambda-cdk/node_modules/chevrotain": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" } }, "node_modules/cargo-lambda-cdk/node_modules/core-js-pure": { - "version": "3.33.2", + "version": "3.39.0", "hasInstallScript": true, "inBundle": true, "license": "MIT", @@ -1948,26 +1974,21 @@ } }, "node_modules/cargo-lambda-cdk/node_modules/js-toml": { - "version": "0.1.1", + "version": "1.0.2", "inBundle": true, "license": "MIT", "dependencies": { - "chevrotain": "^10.4.1", + "chevrotain": "^11.0.3", "xregexp": "^5.1.1" } }, - "node_modules/cargo-lambda-cdk/node_modules/lodash": { + "node_modules/cargo-lambda-cdk/node_modules/lodash-es": { "version": "4.17.21", "inBundle": true, "license": "MIT" }, "node_modules/cargo-lambda-cdk/node_modules/regenerator-runtime": { - "version": "0.14.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/cargo-lambda-cdk/node_modules/regexp-to-ast": { - "version": "0.5.0", + "version": "0.14.1", "inBundle": true, "license": "MIT" }, @@ -2080,12 +2101,9 @@ "dev": true }, "node_modules/constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", - "engines": { - "node": ">= 16.14.0" - } + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -2353,18 +2371,18 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -3329,10 +3347,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3493,9 +3512,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -3682,9 +3701,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -3835,6 +3854,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -4379,27 +4399,22 @@ } }, "@aws-cdk/asset-awscli-v1": { - "version": "2.2.203", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.203.tgz", - "integrity": "sha512-7ZhjD0L62dhWL0yzoLCxvJTU3tLHTz/yEg6GKt3foSj+ljVR1KSP8MuAi+QPb4pT7ZTfVzELMtI36Y/ROuL3ig==" - }, - "@aws-cdk/asset-kubectl-v20": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==" }, "@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", - "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" }, "@aws-cdk/cloud-assembly-schema": { - "version": "36.3.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-36.3.0.tgz", - "integrity": "sha512-mLSYgcMFTNCXrGAD7xob95p9s47/7WwEWUJiexxM46H2GxiijhlhLQJs31AS5uRRP6Cx1DLEu4qayKAUOOVGrw==", + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", "requires": { - "jsonschema": "^1.4.1", - "semver": "^7.6.3" + "jsonschema": "~1.4.1", + "semver": "^7.7.4" }, "dependencies": { "jsonschema": { @@ -4407,7 +4422,7 @@ "bundled": true }, "semver": { - "version": "7.6.3", + "version": "7.7.4", "bundled": true } } @@ -5358,33 +5373,41 @@ } }, "aws-cdk-lib": { - "version": "2.159.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.159.1.tgz", - "integrity": "sha512-zcOyAs3+DTu+CtLehdOgvyosZ7nbLZ+OfBE6uVNMshXm957oXJrLsu6hehLt81TDxfItWYNluFcXkwepZDm6Ng==", - "requires": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^36.0.24", + "version": "2.253.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.0.tgz", + "integrity": "sha512-36nNLOz0WJDWraEzU7zlmYY8nsM+YEroBWpRZ84LZPaeEe4Ozy4RhBvdCz/jIGYtLbtiFnfk+przNW2NNRjz5Q==", + "requires": { + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.3", "ignore": "^5.3.2", - "jsonschema": "^1.4.1", + "jsonschema": "^1.5.0", "mime-types": "^2.1.35", - "minimatch": "^3.1.2", + "minimatch": "^10.2.3", "punycode": "^2.3.1", - "semver": "^7.6.3", - "table": "^6.8.2", - "yaml": "1.10.2" + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" }, "dependencies": { + "@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundled": true, + "requires": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + } + }, "@balena/dockerignore": { "version": "1.0.2", "bundled": true }, "ajv": { - "version": "8.17.1", + "version": "8.18.0", "bundled": true, "requires": { "fast-deep-equal": "^3.1.3", @@ -5409,15 +5432,14 @@ "bundled": true }, "balanced-match": { - "version": "1.0.2", + "version": "4.0.4", "bundled": true }, "brace-expansion": { - "version": "1.1.11", + "version": "5.0.5", "bundled": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" } }, "case": { @@ -5435,10 +5457,6 @@ "version": "1.1.4", "bundled": true }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, "emoji-regex": { "version": "8.0.0", "bundled": true @@ -5448,11 +5466,11 @@ "bundled": true }, "fast-uri": { - "version": "3.0.1", + "version": "3.1.0", "bundled": true }, "fs-extra": { - "version": "11.2.0", + "version": "11.3.3", "bundled": true, "requires": { "graceful-fs": "^4.2.0", @@ -5477,7 +5495,7 @@ "bundled": true }, "jsonfile": { - "version": "6.1.0", + "version": "6.2.0", "bundled": true, "requires": { "graceful-fs": "^4.1.6", @@ -5485,7 +5503,7 @@ } }, "jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "bundled": true }, "lodash.truncate": { @@ -5504,10 +5522,10 @@ } }, "minimatch": { - "version": "3.1.2", + "version": "10.2.5", "bundled": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" } }, "punycode": { @@ -5519,7 +5537,7 @@ "bundled": true }, "semver": { - "version": "7.6.3", + "version": "7.7.4", "bundled": true }, "slice-ansi": { @@ -5548,7 +5566,7 @@ } }, "table": { - "version": "6.8.2", + "version": "6.9.0", "bundled": true, "requires": { "ajv": "^8.0.1", @@ -5562,14 +5580,8 @@ "version": "2.0.1", "bundled": true }, - "uri-js": { - "version": "4.4.1", - "requires": { - "punycode": "^2.1.0" - } - }, "yaml": { - "version": "1.10.2", + "version": "1.10.3", "bundled": true } } @@ -5669,9 +5681,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -5741,15 +5753,15 @@ "dev": true }, "cargo-lambda-cdk": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.22.tgz", - "integrity": "sha512-lpRh4TFR03FSWw/Ji6D3ZD18mAJjeWvU8ST5UdS6RwbdSXQT/0TbEjG/ccMCpqNGtrlx+txQ8WYURgtNbnDKcw==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", "requires": { - "js-toml": "^0.1.1" + "js-toml": "^1.0.2" }, "dependencies": { "@babel/runtime-corejs3": { - "version": "7.23.2", + "version": "7.26.0", "bundled": true, "requires": { "core-js-pure": "^3.30.2", @@ -5757,64 +5769,64 @@ } }, "@chevrotain/cst-dts-gen": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, "@chevrotain/gast": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, + "@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "bundled": true + }, "@chevrotain/types": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true }, "@chevrotain/utils": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true }, "chevrotain": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" } }, "core-js-pure": { - "version": "3.33.2", + "version": "3.39.0", "bundled": true }, "js-toml": { - "version": "0.1.1", + "version": "1.0.2", "bundled": true, "requires": { - "chevrotain": "^10.4.1", + "chevrotain": "^11.0.3", "xregexp": "^5.1.1" } }, - "lodash": { + "lodash-es": { "version": "4.17.21", "bundled": true }, "regenerator-runtime": { - "version": "0.14.0", - "bundled": true - }, - "regexp-to-ast": { - "version": "0.5.0", + "version": "0.14.1", "bundled": true }, "xregexp": { @@ -5899,9 +5911,9 @@ "dev": true }, "constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==" + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" }, "convert-source-map": { "version": "2.0.0", @@ -6096,18 +6108,18 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "requires": { "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -6831,9 +6843,9 @@ "dev": true }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -6955,9 +6967,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -7098,9 +7110,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pirates": { @@ -7200,7 +7212,8 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true }, "shebang-command": { "version": "2.0.0", diff --git a/examples/advanced-appconfig-feature-flags/cdk/package.json b/examples/advanced-appconfig-feature-flags/cdk/package.json index 83cb9552f..0f590c21f 100644 --- a/examples/advanced-appconfig-feature-flags/cdk/package.json +++ b/examples/advanced-appconfig-feature-flags/cdk/package.json @@ -20,8 +20,8 @@ "typescript": "~5.4.5" }, "dependencies": { - "aws-cdk-lib": "2.159.1", - "cargo-lambda-cdk": "^0.0.22", + "aws-cdk-lib": "2.253.0", + "cargo-lambda-cdk": "^0.0.35", "constructs": "^10.0.0", "source-map-support": "^0.5.21" } diff --git a/examples/advanced-appconfig-feature-flags/src/main.rs b/examples/advanced-appconfig-feature-flags/src/main.rs index b7d5e5152..87ec54fac 100644 --- a/examples/advanced-appconfig-feature-flags/src/main.rs +++ b/examples/advanced-appconfig-feature-flags/src/main.rs @@ -35,9 +35,9 @@ async fn function_handler( // Use the feature flag let msg = if config.spanish_response { - format!("{}, in spanish.", quote) + format!("{quote}, in spanish.") } else { - format!("{}.", quote) + format!("{quote}.") }; // Return `Response` (it will be serialized to JSON automatically by the runtime) diff --git a/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml index 69ec04a09..e82dc1d34 100644 --- a/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml +++ b/examples/advanced-sqs-multiple-functions-shared-data/consumer/Cargo.toml @@ -5,9 +5,7 @@ edition = "2021" [dependencies] #aws dependencies -aws-sdk-config = "0.35.0" -aws-sdk-sqs = "0.35.0" -aws_lambda_events = { version = "0.11.1", features = ["sqs"], default-features = false } +aws_lambda_events = { path = "../../../lambda-events", features = ["sqs"], default-features = false } #lambda runtime lambda_runtime = { path = "../../../lambda-runtime" } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml index 76631bbd8..2dd69db13 100644 --- a/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml +++ b/examples/advanced-sqs-multiple-functions-shared-data/pizza_lib/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -serde = { version = "1.0.191", features = ["derive"] } +serde = { version = "1.0.219", features = ["derive"] } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml b/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml index 83aa48ab8..2772f6500 100644 --- a/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml +++ b/examples/advanced-sqs-multiple-functions-shared-data/producer/Cargo.toml @@ -8,14 +8,13 @@ env = { "QUEUE_URL" = "https://changeMe" } [dependencies] #aws dependencies -aws-config = "0.57.1" -aws-sdk-config = "0.35.0" -aws-sdk-sqs = "0.35.0" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-sqs = "1.74.0" #lambda runtime lambda_runtime = { path = "../../../lambda-runtime" } -serde_json = "1.0.108" +serde_json = "1.0.140" tokio = { version = "1", features = ["macros"] } #shared lib -pizza_lib = { path = "../pizza_lib" } \ No newline at end of file +pizza_lib = { path = "../pizza_lib" } diff --git a/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs b/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs index 2a70dce31..6a2883f32 100644 --- a/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs +++ b/examples/advanced-sqs-multiple-functions-shared-data/producer/src/main.rs @@ -20,7 +20,7 @@ async fn main() -> Result<(), Error> { // read the queue url from the environment let queue_url = std::env::var("QUEUE_URL").expect("could not read QUEUE_URL"); // build the config from environment variables (fed by AWS Lambda) - let config = aws_config::from_env().load().await; + let config = aws_config::load_from_env().await; // create our SQS Manager let sqs_manager = SQSManager::new(aws_sdk_sqs::Client::new(&config), queue_url); let sqs_manager_ref = &sqs_manager; diff --git a/examples/advanced-sqs-partial-batch-failures/Cargo.toml b/examples/advanced-sqs-partial-batch-failures/Cargo.toml index 95050b9ad..f02e4efb3 100644 --- a/examples/advanced-sqs-partial-batch-failures/Cargo.toml +++ b/examples/advanced-sqs-partial-batch-failures/Cargo.toml @@ -5,8 +5,6 @@ edition = "2021" [dependencies] serde = "^1" -serde_derive = "^1" -serde_with = { version = "^2", features = ["json"], optional = true } serde_json = "^1" aws_lambda_events = { path = "../../lambda-events" } lambda_runtime = { path = "../../lambda-runtime" } diff --git a/examples/advanced-sqs-partial-batch-failures/src/main.rs b/examples/advanced-sqs-partial-batch-failures/src/main.rs index 42bb22533..4af67b8ce 100644 --- a/examples/advanced-sqs-partial-batch-failures/src/main.rs +++ b/examples/advanced-sqs-partial-batch-failures/src/main.rs @@ -44,7 +44,7 @@ async fn main() -> Result<(), Error> { /// Important note: your lambda sqs trigger *needs* to be configured with partial batch response support /// with the ` ReportBatchItemFailures` flag set to true, otherwise failed message will be dropped, /// for more details see: -/// https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#services-sqs-batchfailurereporting +/// /// /// /// Note that if you are looking for parallel processing (multithread) instead of concurrent processing, @@ -55,7 +55,7 @@ where D: DeserializeOwned, R: Future>, { - run(service_fn(|e| batch_handler(|d| f(d), e))).await + run(service_fn(|e| batch_handler(&f, e))).await } /// Helper function to lift the user provided `f` function from message to batch of messages. @@ -96,11 +96,17 @@ where } }, ) - .map(|id| BatchItemFailure { item_identifier: id }) + .map(|id| { + let mut failure_item = BatchItemFailure::default(); + failure_item.item_identifier = id; + failure_item + }) .collect(); - Ok(SqsBatchResponse { - batch_item_failures: failure_items, + Ok({ + let mut response = SqsBatchResponse::default(); + response.batch_item_failures = failure_items; + response }) } @@ -123,7 +129,7 @@ mod test { } #[tokio::test] - async fn test() -> () { + async fn test() { let msg_to_fail: SqsMessageObj = serde_json::from_str( r#"{ "messageId": "1", @@ -140,8 +146,10 @@ mod test { .unwrap(); let lambda_event = LambdaEvent { - payload: SqsEventObj { - records: vec![msg_to_fail, msg_to_succeed], + payload: { + let mut event_object = SqsEventObj::default(); + event_object.records = vec![msg_to_fail, msg_to_succeed]; + event_object }, context: Context::default(), }; diff --git a/examples/basic-cognito-post-confirmation/Cargo.toml b/examples/basic-cognito-post-confirmation/Cargo.toml index 7d2e7ab45..93369e515 100644 --- a/examples/basic-cognito-post-confirmation/Cargo.toml +++ b/examples/basic-cognito-post-confirmation/Cargo.toml @@ -15,8 +15,8 @@ edition = "2021" # and it will keep the alphabetic ordering for you. [dependencies] -aws-config = "1.5.0" -aws-sdk-ses = "1.28.0" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-ses = "1.77.0" aws_lambda_events = { path = "../../lambda-events", default-features = false, features = ["cognito"] } lambda_runtime = { path = "../../lambda-runtime" } diff --git a/examples/basic-error-error-crates-integration/Cargo.toml b/examples/basic-error-error-crates-integration/Cargo.toml index 741ec7137..24fbc8dca 100644 --- a/examples/basic-error-error-crates-integration/Cargo.toml +++ b/examples/basic-error-error-crates-integration/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" anyhow = "1" eyre = "0.6.12" lambda_runtime = { path = "../../lambda-runtime", features = ["anyhow", "eyre", "miette"] } -miette = "7.2.0" +miette = "7.6.0" serde = "1" tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-error-error-crates-integration/src/main.rs b/examples/basic-error-error-crates-integration/src/main.rs index f40485849..176bd54b3 100644 --- a/examples/basic-error-error-crates-integration/src/main.rs +++ b/examples/basic-error-error-crates-integration/src/main.rs @@ -28,7 +28,7 @@ fn miette_error() -> miette::Result<()> { /// Transform an anyhow::Error, eyre::Report, or miette::Report into a lambda_runtime::Diagnostic. /// It does it by enabling the feature `anyhow`, `eyre` or `miette` in the runtime dependency. -/// Those features enable the implementation of `From for Diagnostic` +/// Those features enable the implementation of `From for Diagnostic` /// for `anyhow::Error`, `eyre::Report`, and `miette::Report`. async fn function_handler(event: LambdaEvent) -> Result<(), Diagnostic> { match event.payload.error_type { diff --git a/examples/basic-error-handling/Cargo.toml b/examples/basic-error-handling/Cargo.toml index 1039a1392..a0267f978 100644 --- a/examples/basic-error-handling/Cargo.toml +++ b/examples/basic-error-handling/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" -serde_json = "1.0.81" -simple-error = "0.2.3" +serde = "1.0.219" +serde_json = "1.0.140" +simple-error = "0.3.1" tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-error-handling/src/main.rs b/examples/basic-error-handling/src/main.rs index 3bc769364..f434c9039 100644 --- a/examples/basic-error-handling/src/main.rs +++ b/examples/basic-error-handling/src/main.rs @@ -1,4 +1,4 @@ -/// See https://github.com/awslabs/aws-lambda-rust-runtime for more info on Rust runtime for AWS Lambda +/// See for more info on Rust runtime for AWS Lambda use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -43,7 +43,7 @@ impl std::fmt::Display for CustomError { /// Display the error struct as a JSON string fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let err_as_json = json!(self).to_string(); - write!(f, "{}", err_as_json) + write!(f, "{err_as_json}") } } @@ -66,7 +66,7 @@ pub(crate) async fn func(event: LambdaEvent) -> Result match event.event_type { EventType::SimpleError => { // generate a simple text message error using `simple_error` crate - return Err(Box::new(simple_error::SimpleError::new("A simple error as requested!"))); + Err(Box::new(simple_error::SimpleError::new("A simple error as requested!"))) } EventType::CustomError => { // generate a custom error using our own structure @@ -75,7 +75,7 @@ pub(crate) async fn func(event: LambdaEvent) -> Result req_id: ctx.request_id, msg: "A custom error as requested!".into(), }; - return Err(Box::new(cust_err)); + Err(Box::new(cust_err)) } EventType::ExternalError => { // try to open a non-existent file to get an error and propagate it with `?` @@ -94,7 +94,7 @@ pub(crate) async fn func(event: LambdaEvent) -> Result msg: "OK".into(), }; - return Ok(resp); + Ok(resp) } } } diff --git a/examples/basic-error-thiserror/Cargo.toml b/examples/basic-error-thiserror/Cargo.toml index d7c7d7251..f2b0b4499 100644 --- a/examples/basic-error-thiserror/Cargo.toml +++ b/examples/basic-error-thiserror/Cargo.toml @@ -18,5 +18,5 @@ edition = "2021" lambda_runtime = { path = "../../lambda-runtime" } serde = "1" -thiserror = "1.0.61" +thiserror = "2.0" tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-error-thiserror/src/main.rs b/examples/basic-error-thiserror/src/main.rs index 403309bf4..2c01b833d 100644 --- a/examples/basic-error-thiserror/src/main.rs +++ b/examples/basic-error-thiserror/src/main.rs @@ -1,6 +1,5 @@ use lambda_runtime::{service_fn, Diagnostic, Error, LambdaEvent}; use serde::Deserialize; -use thiserror; #[derive(Deserialize)] struct Request {} @@ -21,7 +20,7 @@ impl From for Diagnostic { }; Diagnostic { error_type: error_type.into(), - error_message: error_message.into(), + error_message, } } } diff --git a/examples/basic-lambda-concurrent-s3/Cargo.toml b/examples/basic-lambda-concurrent-s3/Cargo.toml new file mode 100644 index 000000000..6678c18b0 --- /dev/null +++ b/examples/basic-lambda-concurrent-s3/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-lambda-concurrent-s3" +version = "0.1.0" +edition = "2021" + +[dependencies] +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1" +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-lambda-concurrent-s3/src/main.rs b/examples/basic-lambda-concurrent-s3/src/main.rs new file mode 100644 index 000000000..ee8a07c4e --- /dev/null +++ b/examples/basic-lambda-concurrent-s3/src/main.rs @@ -0,0 +1,38 @@ +use aws_config::BehaviorVersion; +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + bucket: String, + key: String, + body: String, +} + +#[derive(Serialize)] +struct Response { + message: String, +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let s3_client = aws_sdk_s3::Client::new(&config); + + lambda_runtime::run_concurrent(service_fn(move |event: LambdaEvent| { + let s3_client = s3_client.clone(); // cheap clone, no Arc needed + async move { + s3_client + .put_object() + .bucket(&event.payload.bucket) + .key(&event.payload.key) + .body(event.payload.body.into_bytes().into()) + .send() + .await?; + Ok::(Response { + message: "uploaded".into(), + }) + } + })) + .await +} diff --git a/examples/basic-lambda-concurrent/Cargo.toml b/examples/basic-lambda-concurrent/Cargo.toml new file mode 100644 index 000000000..7c770aead --- /dev/null +++ b/examples/basic-lambda-concurrent/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "basic-lambda-concurrent" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = "1.0.219" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-lambda-concurrent/src/main.rs b/examples/basic-lambda-concurrent/src/main.rs new file mode 100644 index 000000000..0b106b961 --- /dev/null +++ b/examples/basic-lambda-concurrent/src/main.rs @@ -0,0 +1,89 @@ +// This example requires the following input to succeed: +// { "command": "do something" } + +use lambda_runtime::{service_fn, tracing, Diagnostic, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + command: String, +} + +#[derive(Serialize)] +struct Response { + req_id: String, + msg: String, +} + +#[derive(Debug)] +struct HandlerError(String); + +impl std::fmt::Display for HandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Diagnostic { + fn from(e: HandlerError) -> Diagnostic { + Diagnostic { + error_type: "HandlerError".into(), + error_message: e.0, + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let max_concurrency = std::env::var("AWS_LAMBDA_MAX_CONCURRENCY").unwrap_or_else(|_| "not set".to_string()); + tracing::info!(AWS_LAMBDA_MAX_CONCURRENCY = %max_concurrency, "starting concurrent handler"); + + let func = service_fn(my_handler); + if let Err(err) = lambda_runtime::run_concurrent(func).await { + tracing::error!(error = %err, "run error"); + return Err(err); + } + Ok(()) +} + +pub(crate) async fn my_handler(event: LambdaEvent) -> Result { + let command = event.payload.command; + + if command == "fail" { + return Err(HandlerError("simulated handler error".into())); + } + + let resp = Response { + req_id: event.context.request_id, + msg: format!("Command {command} executed."), + }; + + Ok(resp) +} + +#[cfg(test)] +mod tests { + use crate::{my_handler, Request}; + use lambda_runtime::{Context, LambdaEvent}; + + #[tokio::test] + async fn response_is_good_for_simple_input() { + let id = "ID"; + + let mut context = Context::default(); + context.request_id = id.to_string(); + + let payload = Request { + command: "X".to_string(), + }; + let event = LambdaEvent { payload, context }; + + let result = my_handler(event).await.unwrap(); + + assert_eq!(result.msg, "Command X executed."); + assert_eq!(result.req_id, id.to_string()); + } +} diff --git a/examples/basic-lambda-external-runtime/Cargo.toml b/examples/basic-lambda-external-runtime/Cargo.toml index d6d023d8a..40f24d81b 100644 --- a/examples/basic-lambda-external-runtime/Cargo.toml +++ b/examples/basic-lambda-external-runtime/Cargo.toml @@ -4,11 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -async-channel = "1.8.0" -futures-lite = "1.13.0" +async-channel = "2.5.0" +futures-lite = "2.6.0" lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.163" -tokio = "1.28.2" - -[dev-dependencies] -tokio-test = "0.4.2" +serde = "1.0.219" +tokio = "1.46.1" diff --git a/examples/basic-lambda-external-runtime/src/main.rs b/examples/basic-lambda-external-runtime/src/main.rs index bd3b4e6c6..87891ebc4 100644 --- a/examples/basic-lambda-external-runtime/src/main.rs +++ b/examples/basic-lambda-external-runtime/src/main.rs @@ -53,7 +53,7 @@ fn main() -> Result<(), io::Error> { my_runtime(move || future::block_on(app_runtime_task(lambda_rx.clone(), shutdown_tx.clone()))); // Block the main thread until a shutdown signal is received. - future::block_on(shutdown_rx.recv()).map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err))) + future::block_on(shutdown_rx.recv()).map_err(|err| io::Error::other(format!("{err:?}"))) } pub(crate) async fn my_handler(event: LambdaEvent) -> Result { @@ -63,7 +63,7 @@ pub(crate) async fn my_handler(event: LambdaEvent) -> Result Result<(), Error> { tracing::init_default_subscriber(); let func = service_fn(my_handler); - lambda_runtime::run(func).await?; + if let Err(err) = lambda_runtime::run(func).await { + eprintln!("run error: {:?}", err); + return Err(err); + } Ok(()) } @@ -39,7 +42,7 @@ pub(crate) async fn my_handler(event: LambdaEvent) -> Result, _size: u32) -> Vec { mod tests { use super::*; use async_trait::async_trait; - use aws_lambda_events::s3::object_lambda::Configuration; - use aws_lambda_events::s3::object_lambda::HeadObjectContext; - use aws_lambda_events::s3::object_lambda::ListObjectsContext; - use aws_lambda_events::s3::object_lambda::ListObjectsV2Context; - use aws_lambda_events::s3::object_lambda::UserIdentity; - use aws_lambda_events::s3::object_lambda::UserRequest; - use aws_lambda_events::serde_json::json; + use aws_lambda_events::s3::object_lambda::{ + Configuration, HeadObjectContext, ListObjectsContext, ListObjectsV2Context, UserIdentity, UserRequest, + }; use lambda_runtime::{Context, LambdaEvent}; use mockall::mock; - use s3::GetFile; - use s3::SendFile; + use s3::{GetFile, SendFile}; + use serde_json::json; #[tokio::test] async fn response_is_good() { mock! { FakeS3Client {} - #[async_trait] impl GetFile for FakeS3Client { - pub fn get_file(&self, url: String) -> Result, Box>; + fn get_file(&self, url: String) -> Result, Box>; } #[async_trait] impl SendFile for FakeS3Client { - pub async fn send_file(&self, route: String, token: String, vec: Vec) -> Result>; + async fn send_file(&self, route: String, token: String, vec: Vec) -> Result>; } } @@ -118,9 +113,7 @@ mod tests { .returning(|_1| Ok("IMAGE".into())); mock.expect_send_file() - .withf(|r, t, by| { - return r.eq("O_ROUTE") && t.eq("O_TOKEN") && by == "THUMBNAIL".as_bytes(); - }) + .withf(|r, t, by| r.eq("O_ROUTE") && t.eq("O_TOKEN") && by == "THUMBNAIL".as_bytes()) .returning(|_1, _2, _3| Ok("File sent.".to_string())); let payload = get_s3_event(); @@ -133,24 +126,28 @@ mod tests { } fn get_s3_event() -> S3ObjectLambdaEvent { - return S3ObjectLambdaEvent { - x_amz_request_id: ("ID".to_string()), - head_object_context: (Some(HeadObjectContext::default())), - list_objects_context: (Some(ListObjectsContext::default())), - get_object_context: (Some(GetObjectContext { - input_s3_url: ("S3_URL".to_string()), - output_route: ("O_ROUTE".to_string()), - output_token: ("O_TOKEN".to_string()), - })), - list_objects_v2_context: (Some(ListObjectsV2Context::default())), - protocol_version: ("VERSION".to_string()), - user_identity: (UserIdentity::default()), - user_request: (UserRequest::default()), - configuration: (Configuration { - access_point_arn: ("APRN".to_string()), - supporting_access_point_arn: ("SAPRN".to_string()), - payload: (json!(null)), - }), + let mut event = S3ObjectLambdaEvent::default(); + event.x_amz_request_id = "ID".to_string(); + event.head_object_context = Some(HeadObjectContext::default()); + event.list_objects_context = Some(ListObjectsContext::default()); + event.get_object_context = Some({ + let mut context = GetObjectContext::default(); + context.input_s3_url = "S3_URL".to_string(); + context.output_route = "O_ROUTE".to_string(); + context.output_token = "O_TOKEN".to_string(); + context + }); + event.list_objects_v2_context = Some(ListObjectsV2Context::default()); + event.protocol_version = "VERSION".to_string(); + event.user_identity = UserIdentity::default(); + event.user_request = UserRequest::default(); + event.configuration = { + let mut configuration = Configuration::default(); + configuration.access_point_arn = "APRN".to_string(); + configuration.supporting_access_point_arn = "SAPRN".to_string(); + configuration.payload = json!(null); + configuration }; + event } } diff --git a/examples/basic-s3-object-lambda-thumbnail/src/s3.rs b/examples/basic-s3-object-lambda-thumbnail/src/s3.rs index daba37396..69b46ec28 100644 --- a/examples/basic-s3-object-lambda-thumbnail/src/s3.rs +++ b/examples/basic-s3-object-lambda-thumbnail/src/s3.rs @@ -1,6 +1,8 @@ use async_trait::async_trait; -use aws_sdk_s3::{operation::write_get_object_response::WriteGetObjectResponseError, Client as S3Client}; -use aws_smithy_http::{byte_stream::ByteStream, result::SdkError}; +use aws_sdk_s3::{ + error::SdkError, operation::write_get_object_response::WriteGetObjectResponseError, primitives::ByteStream, + Client as S3Client, +}; use lambda_runtime::tracing; use std::{error, io::Read}; @@ -17,12 +19,17 @@ impl GetFile for S3Client { fn get_file(&self, url: String) -> Result, Box> { tracing::info!("get file url {}", url); - let resp = ureq::get(&url).call()?; - let len: usize = resp.header("Content-Length").unwrap().parse()?; + let mut res = ureq::get(&url).call()?; + let len: usize = res + .headers() + .get("Content-Length") + .and_then(|h| h.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap(); let mut bytes: Vec = Vec::with_capacity(len); - std::io::Read::take(resp.into_reader(), 10_000_000).read_to_end(&mut bytes)?; + std::io::Read::take(res.body_mut().as_reader(), 10_000_000).read_to_end(&mut bytes)?; tracing::info!("got {} bytes", bytes.len()); @@ -46,9 +53,8 @@ impl SendFile for S3Client { .send() .await; - if write.is_err() { - let sdk_error = write.err().unwrap(); - check_error(sdk_error); + if let Err(err) = write { + check_error(err); Err("WriteGetObjectResponse creation error".into()) } else { Ok("File sent.".to_string()) @@ -65,16 +71,16 @@ fn check_error(error: SdkError) { tracing::info!("DispatchFailure"); if err.is_io() { tracing::info!("IO error"); - }; + } if err.is_timeout() { tracing::info!("Timeout error"); - }; + } if err.is_user() { tracing::info!("User error"); - }; - if err.is_other().is_some() { + } + if err.is_other() { tracing::info!("Other error"); - }; + } } SdkError::ResponseError(_err) => tracing::info!("ResponseError"), SdkError::TimeoutError(_err) => tracing::info!("TimeoutError"), diff --git a/examples/basic-s3-thumbnail/Cargo.toml b/examples/basic-s3-thumbnail/Cargo.toml index 4b9ef3dad..fdbd79be1 100644 --- a/examples/basic-s3-thumbnail/Cargo.toml +++ b/examples/basic-s3-thumbnail/Cargo.toml @@ -17,17 +17,13 @@ edition = "2021" [dependencies] aws_lambda_events = { path = "../../lambda-events" } lambda_runtime = { path = "../../lambda-runtime" } -serde = "1" tokio = { version = "1", features = ["macros"] } -aws-config = "0.55" -aws-smithy-http = "0.55.3" -aws-sdk-s3 = "0.28" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.96.0" thumbnailer = "0.5.1" -mime = "0.3.16" -async-trait = "0.1.68" -webp = "=0.2.1" +mime = "0.3.17" +async-trait = "0.1.88" [dev-dependencies] -mockall = "0.11" -tokio-test = "0.4" +mockall = "0.13.1" chrono = "0.4" diff --git a/examples/basic-s3-thumbnail/src/main.rs b/examples/basic-s3-thumbnail/src/main.rs index 3eb5bfe9e..0d647b05c 100644 --- a/examples/basic-s3-thumbnail/src/main.rs +++ b/examples/basic-s3-thumbnail/src/main.rs @@ -139,16 +139,11 @@ mod tests { use super::*; use async_trait::async_trait; //use aws_lambda_events::chrono::DateTime; - use aws_lambda_events::s3::S3Bucket; - use aws_lambda_events::s3::S3Entity; - use aws_lambda_events::s3::S3Object; - use aws_lambda_events::s3::S3RequestParameters; - use aws_lambda_events::s3::S3UserIdentity; + use aws_lambda_events::s3::{S3Bucket, S3Entity, S3Object, S3RequestParameters, S3UserIdentity}; use aws_sdk_s3::operation::get_object::GetObjectError; use lambda_runtime::{Context, LambdaEvent}; use mockall::mock; - use s3::GetFile; - use s3::PutFile; + use s3::{GetFile, PutFile}; #[tokio::test] async fn response_is_good() { @@ -163,11 +158,11 @@ mod tests { #[async_trait] impl GetFile for FakeS3Client { - pub async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; } #[async_trait] impl PutFile for FakeS3Client { - pub async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; + async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; } } @@ -178,60 +173,65 @@ mod tests { .returning(|_1, _2| Ok("IMAGE".into())); mock.expect_put_file() - .withf(|bu, ke, by| { - return bu.eq("test-bucket-thumbs") && ke.eq(key) && by.eq("THUMBNAIL".as_bytes()); - }) + .withf(|bu, ke, by| bu.eq("test-bucket-thumbs") && ke.eq(key) && by.eq("THUMBNAIL".as_bytes())) .return_const(Ok("Done".to_string())); let payload = get_s3_event("ObjectCreated", bucket, key); let event = LambdaEvent { payload, context }; - let result = function_handler(event, 10, &mock).await.unwrap(); + function_handler(event, 10, &mock).await.unwrap(); - assert_eq!((), result); + assert_eq!((), ()); } fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event { - return S3Event { - records: (vec![get_s3_event_record(event_name, bucket_name, object_key)]), - }; + let mut event = S3Event::default(); + event.records = vec![get_s3_event_record(event_name, bucket_name, object_key)]; + event } fn get_s3_event_record(event_name: &str, bucket_name: &str, object_key: &str) -> S3EventRecord { - let s3_entity = S3Entity { - schema_version: (Some(String::default())), - configuration_id: (Some(String::default())), - bucket: (S3Bucket { - name: (Some(bucket_name.to_string())), - owner_identity: Some(S3UserIdentity { - principal_id: (Some(String::default())), - }), - arn: (Some(String::default())), - }), - object: (S3Object { - key: (Some(object_key.to_string())), - size: (Some(1)), - url_decoded_key: (Some(String::default())), - version_id: (Some(String::default())), - e_tag: (Some(String::default())), - sequencer: (Some(String::default())), - }), + let mut s3_bucket = S3Bucket::default(); + s3_bucket.name = (Some(bucket_name.to_string())); + s3_bucket.owner_identity = { + let mut s3_user_identity = S3UserIdentity::default(); + s3_user_identity.principal_id = Some(String::default()); + Some(s3_user_identity) }; - - return S3EventRecord { - event_version: (Some(String::default())), - event_source: (Some(String::default())), - aws_region: (Some(String::default())), - event_time: (chrono::DateTime::default()), - event_name: (Some(event_name.to_string())), - principal_id: (S3UserIdentity { - principal_id: (Some("X".to_string())), - }), - request_parameters: (S3RequestParameters { - source_ip_address: (Some(String::default())), - }), - response_elements: (HashMap::new()), - s3: (s3_entity), + s3_bucket.arn = Some(String::default()); + + let mut s3_object = S3Object::default(); + s3_object.key = Some(object_key.to_string()); + s3_object.size = Some(1); + s3_object.url_decoded_key = Some(String::default()); + s3_object.version_id = Some(String::default()); + s3_object.e_tag = Some(String::default()); + s3_object.sequencer = Some(String::default()); + + let mut s3_entity = S3Entity::default(); + s3_entity.schema_version = Some(String::default()); + s3_entity.configuration_id = Some(String::default()); + s3_entity.bucket = s3_bucket; + s3_entity.object = s3_object; + + let mut s3_event_record = S3EventRecord::default(); + s3_event_record.event_version = Some(String::default()); + s3_event_record.event_source = Some(String::default()); + s3_event_record.aws_region = Some(String::default()); + s3_event_record.event_time = chrono::DateTime::default(); + s3_event_record.event_name = Some(event_name.to_string()); + s3_event_record.principal_id = { + let mut s3_user_identity = S3UserIdentity::default(); + s3_user_identity.principal_id = Some("X".to_string()); + s3_user_identity + }; + s3_event_record.request_parameters = { + let mut s3_request_parameters = S3RequestParameters::default(); + s3_request_parameters.source_ip_address = Some(String::default()); + s3_request_parameters }; + s3_event_record.response_elements = HashMap::new(); + s3_event_record.s3 = s3_entity; + s3_event_record } } diff --git a/examples/basic-s3-thumbnail/src/s3.rs b/examples/basic-s3-thumbnail/src/s3.rs index 0dd8629d9..1a7593710 100644 --- a/examples/basic-s3-thumbnail/src/s3.rs +++ b/examples/basic-s3-thumbnail/src/s3.rs @@ -1,7 +1,5 @@ use async_trait::async_trait; -use aws_sdk_s3::operation::get_object::GetObjectError; -use aws_sdk_s3::Client as S3Client; -use aws_smithy_http::byte_stream::ByteStream; +use aws_sdk_s3::{operation::get_object::GetObjectError, primitives::ByteStream, Client as S3Client}; use lambda_runtime::tracing; #[async_trait] @@ -45,7 +43,7 @@ impl PutFile for S3Client { let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await; match result { - Ok(_) => Ok(format!("Uploaded a file with key {} into {}", key, bucket)), + Ok(_) => Ok(format!("Uploaded a file with key {key} into {bucket}")), Err(err) => Err(err.into_service_error().meta().message().unwrap().to_string()), } } diff --git a/examples/basic-sdk/Cargo.toml b/examples/basic-sdk/Cargo.toml index 454a970f4..6e680aaf9 100644 --- a/examples/basic-sdk/Cargo.toml +++ b/examples/basic-sdk/Cargo.toml @@ -7,12 +7,11 @@ edition = "2021" [dependencies] async-trait = "0.1" -aws-config = "0.54" -aws-sdk-s3 = "0.24" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.96.0" lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } [dev-dependencies] -mockall = "0.11.3" -tokio-test = "0.4.2" \ No newline at end of file +mockall = "0.13.1" diff --git a/examples/basic-sdk/README.md b/examples/basic-sdk/README.md index 483f6951f..ebe70dc7e 100644 --- a/examples/basic-sdk/README.md +++ b/examples/basic-sdk/README.md @@ -1,7 +1,7 @@ ## basic-sdk example -This is an sample function that uses the [AWS SDK](https://github.com/awslabs/aws-sdk-rust) to +This is an sample function that uses the [AWS SDK](https://github.com/aws/aws-sdk-rust) to list the contents of an S3 bucket specified by the invoker. It uses standard credentials as defined in the function's execution role to make calls against S3. diff --git a/examples/basic-sdk/src/main.rs b/examples/basic-sdk/src/main.rs index d49c84e10..a021f4c20 100644 --- a/examples/basic-sdk/src/main.rs +++ b/examples/basic-sdk/src/main.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use aws_sdk_s3::{output::ListObjectsV2Output, Client as S3Client}; +use aws_sdk_s3::{operation::list_objects_v2::ListObjectsV2Output, Client as S3Client}; use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; @@ -52,8 +52,7 @@ async fn my_handler(event: LambdaEvent, client: &T) -> let objects_rsp = client.list_objects(&bucket).await?; let objects: Vec<_> = objects_rsp .contents() - .ok_or("missing objects in list-objects-v2 response")? - .into_iter() + .iter() .filter_map(|o| o.key().map(|k| k.to_string())) .collect(); @@ -71,7 +70,7 @@ async fn my_handler(event: LambdaEvent, client: &T) -> #[cfg(test)] mod tests { use super::*; - use aws_sdk_s3::model::Object; + use aws_sdk_s3::types::Object; use lambda_runtime::{Context, LambdaEvent}; use mockall::predicate::eq; diff --git a/examples/basic-shared-resource/Cargo.toml b/examples/basic-shared-resource/Cargo.toml index 2aad58865..514bef32c 100644 --- a/examples/basic-shared-resource/Cargo.toml +++ b/examples/basic-shared-resource/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-snapstart/Cargo.toml b/examples/basic-snapstart/Cargo.toml new file mode 100644 index 000000000..77ec94e58 --- /dev/null +++ b/examples/basic-snapstart/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "basic-snapstart" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.219" +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-snapstart/Dockerfile b/examples/basic-snapstart/Dockerfile new file mode 100644 index 000000000..50d8068d2 --- /dev/null +++ b/examples/basic-snapstart/Dockerfile @@ -0,0 +1,38 @@ +# Container image for the basic-snapstart example. +# +# SnapStart for functions packaged as OCI images requires a custom base image +# whose runtime implements the restore lifecycle — which `lambda_runtime` now +# does. This image builds the example as the Lambda `bootstrap` on top of the +# AWS-provided base (`provided:al2023`), which supports SnapStart. +# +# Build from the REPOSITORY ROOT (the example uses path dependencies on the +# workspace crates, so the build context must include them): +# +# docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart . +# +# Then push to ECR and create the function with SnapStart enabled +# (PublishedVersions) and the architecture matching your build host. + +# ---- build stage ---- +FROM public.ecr.aws/lambda/provided:al2023 AS builder + +RUN dnf install -y gcc && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy only the workspace crates this example depends on via path, then the +# example itself: lambda_runtime -> lambda_runtime_api_client. +COPY Cargo.* /build/ +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY examples/basic-snapstart /build/examples/basic-snapstart + +WORKDIR /build/examples/basic-snapstart +RUN cargo build --release + +# ---- runtime stage ---- +FROM public.ecr.aws/lambda/provided:al2023 + +COPY --from=builder /build/examples/basic-snapstart/target/release/basic-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [ "/var/runtime/bootstrap" ] diff --git a/examples/basic-snapstart/README.md b/examples/basic-snapstart/README.md new file mode 100644 index 000000000..c6a9ee40f --- /dev/null +++ b/examples/basic-snapstart/README.md @@ -0,0 +1,58 @@ +# AWS Lambda SnapStart example (lambda_runtime) + +This example shows how to use AWS Lambda **SnapStart** with the `lambda_runtime` +crate. It implements the [`SnapStartResource`] trait on an `AppState` and +registers it on the runtime, so the runtime runs the resource's `before_snapshot` +hook before the VM snapshot and its `after_restore` hook after each restore. + +SnapStart reduces cold-start latency by snapshotting the initialized execution +environment and restoring from it. Resources created during init (connections, +credentials, unique values) may be invalid after restore — `before_snapshot` / +`after_restore` are where you release and re-establish them. When SnapStart is +not enabled, the hooks are never called and the function behaves normally. + +## Why a container image? + +SnapStart for functions packaged as OCI images requires a custom base image +whose runtime implements the restore lifecycle — which `lambda_runtime` now does. +This example ships a `Dockerfile` that builds the binary as the Lambda +`bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`, which supports +SnapStart. + +## Build & Deploy + +Build the image from the **repository root** (the example depends on the +workspace crates via path, so the build context must include them): + +```sh +docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart . +``` + +Push it to ECR and create the function from the image: + +```sh +aws ecr create-repository --repository-name basic-snapstart +docker tag basic-snapstart:latest .dkr.ecr..amazonaws.com/basic-snapstart:latest +docker push .dkr.ecr..amazonaws.com/basic-snapstart:latest + +aws lambda create-function \ + --function-name basic-snapstart \ + --package-type Image \ + --code ImageUri=.dkr.ecr..amazonaws.com/basic-snapstart:latest \ + --role \ + --snap-start ApplyOn=PublishedVersions +``` + +SnapStart applies to **published versions**, so publish a version to trigger +snapshot creation, then invoke that version (or an alias pointing at it): + +```sh +aws lambda publish-version --function-name basic-snapstart +aws lambda invoke --function-name basic-snapstart:1 --payload '{"name":"world"}' out.json +``` + +## Architecture + +The compiled `bootstrap` is architecture-specific. Build on (or for) the same +architecture as the target function — `provided:al2023` is available for both +`x86_64` and `arm64`. diff --git a/examples/basic-snapstart/src/main.rs b/examples/basic-snapstart/src/main.rs new file mode 100644 index 000000000..ed9f39f6c --- /dev/null +++ b/examples/basic-snapstart/src/main.rs @@ -0,0 +1,85 @@ +// This example demonstrates using SnapStart with the lambda_runtime crate. +// +// Implement the `SnapStartResource` trait on the types that hold +// snapshot-sensitive state (connections, credentials, cached values) and +// register them on the runtime. When deployed with SnapStart enabled, the +// runtime will: +// 1. Initialize and create resources (e.g., database connections) +// 2. Run each resource's `before_snapshot` hook (reverse registration order) +// before the VM snapshot +// 3. Call `/restore/next`, which blocks until the VM is restored +// 4. Run each resource's `after_restore` hook (registration order) after restore +// 5. Enter the normal invocation loop +// +// When SnapStart is NOT enabled, the hooks are never called and the runtime +// behaves exactly as before. + +use lambda_runtime::{service_fn, tracing, BoxFuture, Error, LambdaEvent, Runtime, SnapStartResource}; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +#[derive(Deserialize)] +struct Request { + name: String, +} + +#[derive(Serialize)] +struct Response { + message: String, + invocation_count: u64, +} + +struct AppState { + counter: AtomicU64, +} + +impl AppState { + fn new() -> Self { + Self { + counter: AtomicU64::new(0), + } + } +} + +impl SnapStartResource for AppState { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Releasing resources before snapshot"); + self.counter.store(0, Ordering::Relaxed); + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Re-establishing resources after restore"); + Ok(()) + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let state = Arc::new(AppState::new()); + + let state_ref = state.clone(); + let handler = service_fn(move |event: LambdaEvent| { + let state = state_ref.clone(); + async move { + let count = state.counter.fetch_add(1, Ordering::Relaxed) + 1; + Ok::<_, Error>(Response { + message: format!("Hello, {}!", event.payload.name), + invocation_count: count, + }) + } + }); + + Runtime::new(handler) + .register_snapstart_resource(state.clone()) + .run() + .await?; + Ok(()) +} diff --git a/examples/basic-sqs/Cargo.toml b/examples/basic-sqs/Cargo.toml index 0df7d8e22..36efee366 100644 --- a/examples/basic-sqs/Cargo.toml +++ b/examples/basic-sqs/Cargo.toml @@ -17,5 +17,5 @@ edition = "2021" [dependencies] aws_lambda_events = { path = "../../lambda-events" } lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } diff --git a/examples/basic-streaming-response/src/main.rs b/examples/basic-streaming-response/src/main.rs index 8533c8e33..fe112b803 100644 --- a/examples/basic-streaming-response/src/main.rs +++ b/examples/basic-streaming-response/src/main.rs @@ -7,7 +7,7 @@ use serde_json::Value; use std::{thread, time::Duration}; async fn func(_event: LambdaEvent) -> Result, Error> { - let messages = vec!["Hello", "world", "from", "Lambda!"]; + let messages = ["Hello", "world", "from", "Lambda!"]; let (mut tx, rx) = channel(); diff --git a/examples/basic-tenant-id/Cargo.toml b/examples/basic-tenant-id/Cargo.toml new file mode 100644 index 000000000..2b1144cb6 --- /dev/null +++ b/examples/basic-tenant-id/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "basic-tenant-id" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime" } +serde_json = "1.0" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } diff --git a/examples/basic-tenant-id/README.md b/examples/basic-tenant-id/README.md new file mode 100644 index 000000000..eb233e75e --- /dev/null +++ b/examples/basic-tenant-id/README.md @@ -0,0 +1,38 @@ +# Basic Tenant ID Example + +This example demonstrates how to access and use tenant ID information in a Lambda function. + +## Key Features + +- Extracts tenant ID from Lambda runtime headers +- Includes tenant ID in tracing logs +- Returns tenant ID in the response +- Handles cases where tenant ID is not provided + +## Usage + +The tenant ID is automatically extracted from the `lambda-runtime-aws-tenant-id` header and made available in the Lambda context. + +```rust +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + // Access tenant ID from context + match &context.tenant_id { + Some(tenant_id) => println!("Processing for tenant: {}", tenant_id), + None => println!("No tenant ID provided"), + } + + // ... rest of function logic +} +``` + +## Testing + +You can test this function locally using cargo lambda: + +```bash +cargo lambda invoke --data-ascii '{"test": "data"}' +``` + +The tenant ID will be None when testing locally unless you set up a mock runtime environment with the appropriate headers. diff --git a/examples/basic-tenant-id/src/main.rs b/examples/basic-tenant-id/src/main.rs new file mode 100644 index 000000000..1789bcba3 --- /dev/null +++ b/examples/basic-tenant-id/src/main.rs @@ -0,0 +1,36 @@ +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde_json::{json, Value}; + +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + // Access tenant ID from context + let tenant_info = match &context.tenant_id { + Some(tenant_id) => format!("Processing request for tenant: {}", tenant_id), + None => "No tenant ID provided".to_string(), + }; + + tracing::info!("Request ID: {}", context.request_id); + tracing::info!("Tenant info: {}", tenant_info); + + // Include tenant ID in response + let response = json!({ + "message": "Hello from Lambda!", + "request_id": context.request_id, + "tenant_id": context.tenant_id, + "input": event + }); + + Ok(response) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_target(false) + .without_time() + .init(); + + lambda_runtime::run(service_fn(function_handler)).await +} diff --git a/examples/extension-basic/Cargo.toml b/examples/extension-basic/Cargo.toml index 48e2ed51b..d7cf1f135 100644 --- a/examples/extension-basic/Cargo.toml +++ b/examples/extension-basic/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } diff --git a/examples/extension-combined/Cargo.toml b/examples/extension-combined/Cargo.toml index 2a745c7b6..93aacca10 100644 --- a/examples/extension-combined/Cargo.toml +++ b/examples/extension-combined/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } diff --git a/examples/extension-custom-events/Cargo.toml b/examples/extension-custom-events/Cargo.toml index c2f813c39..dfef4c4b0 100644 --- a/examples/extension-custom-events/Cargo.toml +++ b/examples/extension-custom-events/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } diff --git a/examples/extension-custom-service/Cargo.toml b/examples/extension-custom-service/Cargo.toml index b51eae8e6..8d0e4575d 100644 --- a/examples/extension-custom-service/Cargo.toml +++ b/examples/extension-custom-service/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } diff --git a/examples/extension-internal-flush/Cargo.toml b/examples/extension-internal-flush/Cargo.toml index daadd0eb0..1a0747dd8 100644 --- a/examples/extension-internal-flush/Cargo.toml +++ b/examples/extension-internal-flush/Cargo.toml @@ -8,5 +8,5 @@ anyhow = "1" aws_lambda_events = { path = "../../lambda-events" } lambda-extension = { path = "../../lambda-extension" } lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" -tokio = { version = "1", features = ["macros", "sync"] } +serde = "1.0.219" +tokio = { version = "1.46", features = ["macros", "sync"] } diff --git a/examples/extension-internal-flush/README.md b/examples/extension-internal-flush/README.md index 553f7a3d6..c64086767 100644 --- a/examples/extension-internal-flush/README.md +++ b/examples/extension-internal-flush/README.md @@ -18,9 +18,10 @@ the Lambda service returns the response to the caller immediately. Extensions ma without introducing an observable delay. ## Build & Deploy +Two deploy options for internal extensions: 1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) -2. Build the extension with `cargo lambda build --release` +2. Build a function with the internal extension embedded with `cargo lambda build --release` 3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` The last command will give you an ARN for the extension layer that you can use in your functions. @@ -28,3 +29,18 @@ The last command will give you an ARN for the extension layer that you can use i ## Build for ARM 64 Build the extension with `cargo lambda build --release --arm64` + + +## Test your Function + +From your local: +``` +cargo lambda watch +# in new terminal window +cargo lambda invoke --data-ascii '{"Records":[{"messageId":"MessageID_1","receiptHandle":"MessageReceiptHandle","body":"{\"a\":\"Test\",\"b\":123}"}]}' +``` + +If you have deployed to AWS using the commands in the 'Build & Deploy' section, you can also invoke your function remotely: +``` +cargo lambda invoke extension-internal-flush --remote --data-ascii '{"Records":[{"messageId":"MessageID_1","receiptHandle":"MessageReceiptHandle","body":"{\"a\":\"Test\",\"b\":123}"}]}' +``` \ No newline at end of file diff --git a/examples/extension-internal-flush/src/main.rs b/examples/extension-internal-flush/src/main.rs index ff1d10dab..d20213e15 100644 --- a/examples/extension-internal-flush/src/main.rs +++ b/examples/extension-internal-flush/src/main.rs @@ -2,8 +2,10 @@ use anyhow::anyhow; use aws_lambda_events::sqs::{SqsBatchResponse, SqsEventObj}; use lambda_extension::{service_fn, tracing, Error, Extension, NextEvent}; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use tokio::sync::Mutex; +use tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex, +}; use std::sync::Arc; @@ -102,6 +104,10 @@ async fn main() -> Result<(), Error> { let handler = Arc::new(EventHandler::new(request_done_sender)); tokio::try_join!( + // always poll the handler function first before the flush extension, + // this results in a smaller future due to not needing to track which was polled first + // each time, and also a tiny latency savings + biased; lambda_runtime::run(service_fn(|event| { let handler = handler.clone(); async move { handler.invoke(event).await } diff --git a/examples/extension-logs-basic/Cargo.toml b/examples/extension-logs-basic/Cargo.toml index dccc1ec0c..230ebc7e0 100644 --- a/examples/extension-logs-basic/Cargo.toml +++ b/examples/extension-logs-basic/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros", "rt"] } diff --git a/examples/extension-logs-custom-service/Cargo.toml b/examples/extension-logs-custom-service/Cargo.toml index 1b1eea0a2..421fe9ff3 100644 --- a/examples/extension-logs-custom-service/Cargo.toml +++ b/examples/extension-logs-custom-service/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros"] } diff --git a/examples/extension-logs-kinesis-firehose/Cargo.toml b/examples/extension-logs-kinesis-firehose/Cargo.toml index c6675e5aa..60026008c 100644 --- a/examples/extension-logs-kinesis-firehose/Cargo.toml +++ b/examples/extension-logs-kinesis-firehose/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -tokio = { version = "1.17.0", features = ["full"] } -aws-config = "0.13.0" -aws-sdk-firehose = "0.13.0" +tokio = { version = "1.46.1", features = ["full"] } +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-sdk-firehose = "1.84.0" diff --git a/examples/extension-logs-kinesis-firehose/src/main.rs b/examples/extension-logs-kinesis-firehose/src/main.rs index 7871ce526..c9d8a2e47 100644 --- a/examples/extension-logs-kinesis-firehose/src/main.rs +++ b/examples/extension-logs-kinesis-firehose/src/main.rs @@ -1,4 +1,4 @@ -use aws_sdk_firehose::{model::Record, types::Blob, Client}; +use aws_sdk_firehose::{primitives::Blob, types::Record, Client}; use lambda_extension::{tracing, Error, Extension, LambdaLog, LambdaLogRecord, Service, SharedService}; use std::{future::Future, pin::Pin, task::Poll}; @@ -31,7 +31,12 @@ impl Service> for FirehoseLogsProcessor { for log in logs { match log.record { LambdaLogRecord::Function(record) => { - records.push(Record::builder().data(Blob::new(record.as_bytes())).build()) + match Record::builder().data(Blob::new(record.as_bytes())).build() { + Ok(rec) => records.push(rec), + Err(e) => { + return Box::pin(async move { Err(e.into()) }); + } + } } _ => unreachable!(), } diff --git a/examples/extension-telemetry-basic/Cargo.toml b/examples/extension-telemetry-basic/Cargo.toml index 1b8b1ba4f..a0fb6b875 100644 --- a/examples/extension-telemetry-basic/Cargo.toml +++ b/examples/extension-telemetry-basic/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda-extension = { path = "../../lambda-extension" } -serde = "1.0.136" tokio = { version = "1", features = ["macros", "rt"] } diff --git a/examples/extension-telemetry-generic/Cargo.toml b/examples/extension-telemetry-generic/Cargo.toml new file mode 100644 index 000000000..50806bce7 --- /dev/null +++ b/examples/extension-telemetry-generic/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "extension-telemetry-basic" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda-extension = { path = "../../lambda-extension" } +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/examples/extension-telemetry-generic/README.md b/examples/extension-telemetry-generic/README.md new file mode 100644 index 000000000..337e045c9 --- /dev/null +++ b/examples/extension-telemetry-generic/README.md @@ -0,0 +1,13 @@ +# AWS Lambda Telemetry extension example + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the extension with `cargo lambda build --release --extension` +3. Deploy the extension as a layer with `cargo lambda deploy --extension` + +The last command will give you an ARN for the extension layer that you can use in your functions. + +## Build for ARM 64 + +Build the extension with `cargo lambda build --release --extension --arm64` diff --git a/examples/extension-telemetry-generic/src/main.rs b/examples/extension-telemetry-generic/src/main.rs new file mode 100644 index 000000000..39bc117e5 --- /dev/null +++ b/examples/extension-telemetry-generic/src/main.rs @@ -0,0 +1,29 @@ +use lambda_extension::{service_fn, tracing, Error, Extension, LambdaTelemetry, LambdaTelemetryRecord, SharedService}; + +async fn handler(events: Vec>) -> Result<(), Error> { + for event in events { + match event.record { + LambdaTelemetryRecord::Function(record) => tracing::info!("[logs] [function] {}", record), + LambdaTelemetryRecord::Extension(record) => tracing::info!("[extension] [function] {}", record), + _ => (), + } + } + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + + let telemetry_processor = SharedService::new(service_fn(handler)); + + Extension::new() + .with_telemetry_record_type::() + .with_telemetry_processor(telemetry_processor) + .run() + .await?; + + Ok(()) +} diff --git a/examples/http-axum-apigw-authorizer/Cargo.toml b/examples/http-axum-apigw-authorizer/Cargo.toml index c757aa942..44c50167a 100644 --- a/examples/http-axum-apigw-authorizer/Cargo.toml +++ b/examples/http-axum-apigw-authorizer/Cargo.toml @@ -4,9 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" +axum = "0.8" lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.196" serde_json = "1.0" tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-apigw-authorizer/src/main.rs b/examples/http-axum-apigw-authorizer/src/main.rs index 513a6cd8e..8adb90240 100644 --- a/examples/http-axum-apigw-authorizer/src/main.rs +++ b/examples/http-axum-apigw-authorizer/src/main.rs @@ -1,5 +1,4 @@ use axum::{ - async_trait, extract::{FromRequest, Request}, http::StatusCode, response::Json, @@ -13,7 +12,6 @@ use std::{collections::HashMap, env::set_var}; struct AuthorizerField(String); struct AuthorizerFields(HashMap); -#[async_trait] impl FromRequest for AuthorizerField where S: Send + Sync, @@ -30,7 +28,6 @@ where } } -#[async_trait] impl FromRequest for AuthorizerFields where S: Send + Sync, diff --git a/examples/http-axum-diesel-ssl/.DS_Store b/examples/http-axum-diesel-ssl/.DS_Store deleted file mode 100644 index 06fbd0f6d..000000000 Binary files a/examples/http-axum-diesel-ssl/.DS_Store and /dev/null differ diff --git a/examples/http-axum-diesel-ssl/Cargo.toml b/examples/http-axum-diesel-ssl/Cargo.toml index 693669576..d21df0b95 100755 --- a/examples/http-axum-diesel-ssl/Cargo.toml +++ b/examples/http-axum-diesel-ssl/Cargo.toml @@ -4,16 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" -bb8 = "0.8.0" -diesel = "2.0.3" -diesel-async = { version = "0.2.1", features = ["postgres", "bb8"] } +axum = "0.8" +diesel = "2.2.11" +diesel-async = { version = "0.6.1", features = ["postgres", "bb8"] } lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.159" -futures-util = "0.3.21" -rustls = "0.20.8" -rustls-native-certs = "0.6.2" -tokio = { version = "1.2.0", default-features = false, features = ["macros", "rt-multi-thread"] } -tokio-postgres = "0.7.7" -tokio-postgres-rustls = "0.9.0" \ No newline at end of file +serde = "1.0.219" +futures-util = "0.3.31" +rustls = "0.23.28" +rustls-native-certs = "0.8.1" +tokio = { version = "1.46.1", default-features = false, features = ["macros", "rt-multi-thread"] } +tokio-postgres = "0.7.13" +tokio-postgres-rustls = "0.13.0" diff --git a/examples/http-axum-diesel-ssl/src/main.rs b/examples/http-axum-diesel-ssl/src/main.rs index b340b44d1..395c18433 100755 --- a/examples/http-axum-diesel-ssl/src/main.rs +++ b/examples/http-axum-diesel-ssl/src/main.rs @@ -1,19 +1,18 @@ -use diesel::{ConnectionError, ConnectionResult}; -use futures_util::future::BoxFuture; -use futures_util::FutureExt; -use std::time::Duration; - use axum::{ extract::{Path, State}, response::Json, routing::get, Router, }; -use bb8::Pool; -use diesel::prelude::*; -use diesel_async::{pooled_connection::AsyncDieselConnectionManager, AsyncPgConnection, RunQueryDsl}; +use diesel::{prelude::*, ConnectionError, ConnectionResult}; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, ManagerConfig}, + AsyncPgConnection, RunQueryDsl, +}; +use futures_util::{future::BoxFuture, FutureExt}; use lambda_http::{http::StatusCode, run, tracing, Error}; use serde::{Deserialize, Serialize}; +use std::time::Duration; table! { posts (id) { @@ -40,7 +39,7 @@ struct NewPost { published: bool, } -type AsyncPool = Pool>; +type AsyncPool = Pool; type ServerError = (StatusCode, String); async fn create_post(State(pool): State, Json(post): Json) -> Result, ServerError> { @@ -104,7 +103,10 @@ async fn main() -> Result<(), Error> { // Format for DATABASE_URL=postgres://your_username:your_password@your_host:5432/your_db?sslmode=require let db_url = std::env::var("DATABASE_URL").expect("Env var `DATABASE_URL` not set"); - let mgr = AsyncDieselConnectionManager::::new_with_setup(db_url, establish_connection); + let mut config = ManagerConfig::default(); + config.custom_setup = Box::new(establish_connection); + + let mgr = AsyncDieselConnectionManager::::new_with_config(db_url, config); let pool = Pool::builder() .max_size(10) @@ -129,19 +131,15 @@ fn establish_connection(config: &str) -> BoxFuture BoxFuture rustls::RootCertStore { let mut roots = rustls::RootCertStore::empty(); let certs = rustls_native_certs::load_native_certs().expect("Certs not loadable!"); - let certs: Vec<_> = certs.into_iter().map(|cert| cert.0).collect(); - roots.add_parsable_certificates(&certs); + roots.add_parsable_certificates(certs); roots } diff --git a/examples/http-axum-diesel/Cargo.toml b/examples/http-axum-diesel/Cargo.toml index 39fc813e4..bed367623 100644 --- a/examples/http-axum-diesel/Cargo.toml +++ b/examples/http-axum-diesel/Cargo.toml @@ -4,11 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" -bb8 = "0.8.0" -diesel = "2.0.3" -diesel-async = { version = "0.2.1", features = ["postgres", "bb8"] } +axum = "0.8" +diesel = "2.2.11" +diesel-async = { version = "0.6.1", features = ["postgres", "bb8"] } lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.159" +serde = "1.0.219" tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-diesel/src/main.rs b/examples/http-axum-diesel/src/main.rs index b7247be47..348d75357 100644 --- a/examples/http-axum-diesel/src/main.rs +++ b/examples/http-axum-diesel/src/main.rs @@ -4,9 +4,11 @@ use axum::{ routing::get, Router, }; -use bb8::Pool; use diesel::prelude::*; -use diesel_async::{pooled_connection::AsyncDieselConnectionManager, AsyncPgConnection, RunQueryDsl}; +use diesel_async::{ + pooled_connection::{bb8::Pool, AsyncDieselConnectionManager}, + AsyncPgConnection, RunQueryDsl, +}; use lambda_http::{http::StatusCode, run, tracing, Error}; use serde::{Deserialize, Serialize}; @@ -35,7 +37,7 @@ struct NewPost { published: bool, } -type AsyncPool = Pool>; +type AsyncPool = Pool; type ServerError = (StatusCode, String); async fn create_post(State(pool): State, Json(post): Json) -> Result, ServerError> { diff --git a/examples/http-axum-middleware/Cargo.toml b/examples/http-axum-middleware/Cargo.toml index 228fc0ae4..f39669411 100644 --- a/examples/http-axum-middleware/Cargo.toml +++ b/examples/http-axum-middleware/Cargo.toml @@ -4,10 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" +axum = "0.8" lambda_http = { path = "../../lambda-http", default-features = false, features = [ "apigw_rest", "tracing" ] } -lambda_runtime = { path = "../../lambda-runtime" } serde_json = "1.0" tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum-middleware/src/main.rs b/examples/http-axum-middleware/src/main.rs index b1e92811a..e335c270f 100644 --- a/examples/http-axum-middleware/src/main.rs +++ b/examples/http-axum-middleware/src/main.rs @@ -10,8 +10,7 @@ //! ``` use axum::{response::Json, routing::post, Router}; -use lambda_http::request::RequestContext::ApiGatewayV1; -use lambda_http::{run, tracing, Error}; +use lambda_http::{request::RequestContext::ApiGatewayV1, run, tracing, Error}; use serde_json::{json, Value}; // Sample middleware that logs the request id diff --git a/examples/http-axum-streaming-otel/Cargo.toml b/examples/http-axum-streaming-otel/Cargo.toml new file mode 100644 index 000000000..d917bb033 --- /dev/null +++ b/examples/http-axum-streaming-otel/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "http-axum-streaming-otel" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +bytes = "1" +lambda_http = { path = "../../lambda-http", default-features = false, features = [ + "apigw_http", "tracing", "opentelemetry" +] } +opentelemetry = "0.30" +opentelemetry_sdk = { version = "0.30", features = ["rt-tokio"] } +opentelemetry-stdout = { version = "0.30", features = ["trace"] } +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } +tokio-stream = "0.1.2" +tracing = "0.1" +tracing-opentelemetry = "0.31" +tracing-subscriber = "0.3" diff --git a/examples/http-axum-streaming-otel/README.md b/examples/http-axum-streaming-otel/README.md new file mode 100644 index 000000000..194fe4e4b --- /dev/null +++ b/examples/http-axum-streaming-otel/README.md @@ -0,0 +1,25 @@ +# AWS Lambda Function example + +This example shows how to build a **streaming HTTP response** with `Axum` and +run it on AWS Lambda using a custom runtime with OpenTelemetry (OTel) support. + +Tracing data is exported as console log entries visible in CloudWatch. Note that +CloudWatch assigns a `Timestamp` to each log entry based on when it receives the +data (batch exported). To see when work actually occurred, look at the span's +event attributes, which include the precise local timestamps of those events. + +## Build & Deploy + +1. Install + [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with: + - `cargo lambda deploy --enable-function-url --iam-role YOUR_ROLE` to stream words +4. Enable Lambda streaming response on Lambda console: change the function url's + invoke mode to `RESPONSE_STREAM` +5. Verify the function works: `curl -N `. The results should be + streamed back with 0.5 second pause between each word. + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-streaming-otel/src/main.rs b/examples/http-axum-streaming-otel/src/main.rs new file mode 100644 index 000000000..64f4e49e4 --- /dev/null +++ b/examples/http-axum-streaming-otel/src/main.rs @@ -0,0 +1,106 @@ +//! # Example: Axum Streaming Responses on AWS Lambda with OTel +//! +//! Demonstrates serving **incremental streaming responses** from Axum handlers +//! running in AWS Lambda using a **custom** `lambda_runtime::Runtime` with +//! OpenTelemetry (OTel) support. +//! +//! - Runs with a custom `Runtime` + `StreamAdapter`, which convert Axum +//! responses into streaming bodies delivered as data is produced (unlike the +//! default `run_with_streaming_response` helper). + +use axum::{ + body::Body, + http::{ + self, + header::{CACHE_CONTROL, CONTENT_TYPE}, + StatusCode, + }, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use bytes::Bytes; +use core::{convert::Infallible, time::Duration}; +use lambda_http::{ + lambda_runtime::{ + layers::{OpenTelemetryFaasTrigger, OpenTelemetryLayer as OtelLayer}, + tracing::Instrument, + Runtime, + }, + tracing, Error, StreamAdapter, +}; +use opentelemetry::trace::TracerProvider; +use opentelemetry_sdk::trace; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tracing_subscriber::prelude::*; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("{0}")] + Http(#[from] http::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +#[tracing::instrument(skip_all)] +async fn stream_words() -> Result { + let (tx, rx) = mpsc::channel::>(8); + let body = Body::from_stream(ReceiverStream::new(rx)); + + tokio::spawn( + async move { + for (idx, msg) in ["Hello", "world", "from", "Lambda!"].iter().enumerate() { + tokio::time::sleep(Duration::from_millis(500)).await; + let line = format!("{msg}\n"); + tracing::info!(chunk.idx = idx, bytes = line.len(), "emit"); + if tx.send(Ok(Bytes::from(line))).await.is_err() { + break; + } + } + } + .instrument(tracing::info_span!("producer.stream_words")), + ); + + Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CACHE_CONTROL, "no-cache") + .body(body)?) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // Set up OpenTelemetry tracer provider that writes spans to stdout for + // debugging purposes + let exporter = opentelemetry_stdout::SpanExporter::default(); + let tracer_provider = trace::SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .build(); + + // Set up link between OpenTelemetry and tracing crate + tracing_subscriber::registry() + .with(tracing_opentelemetry::OpenTelemetryLayer::new( + tracer_provider.tracer("my-streaming-app"), + )) + .init(); + + let svc = Router::new().route("/", get(stream_words)); + + // Initialize the Lambda runtime and add OpenTelemetry tracing + let runtime = Runtime::new(StreamAdapter::from(svc)).layer( + OtelLayer::new(|| { + if let Err(err) = tracer_provider.force_flush() { + eprintln!("Error flushing traces: {err:#?}"); + } + }) + .with_trigger(OpenTelemetryFaasTrigger::Http), + ); + + runtime.run().await +} diff --git a/examples/http-axum-streaming/Cargo.toml b/examples/http-axum-streaming/Cargo.toml new file mode 100644 index 000000000..a951562b0 --- /dev/null +++ b/examples/http-axum-streaming/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "http-axum-streaming" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +bytes = "1" +lambda_http = { path = "../../lambda-http", default-features = false, features = [ + "apigw_http", "tracing" +] } +thiserror = "2.0" +tokio = { version = "1", features = ["macros"] } +tokio-stream = "0.1.2" diff --git a/examples/http-axum-streaming/README.md b/examples/http-axum-streaming/README.md new file mode 100644 index 000000000..fe7e573d7 --- /dev/null +++ b/examples/http-axum-streaming/README.md @@ -0,0 +1,20 @@ +# AWS Lambda Function example + +This example demonstrates building a **streaming** HTTP response with Axum, +deployed on AWS Lambda using a custom runtime. + +## Build & Deploy + +1. Install + [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with: + - `cargo lambda deploy --enable-function-url --iam-role YOUR_ROLE` to stream words +4. Enable Lambda streaming response on Lambda console: change the function url's + invoke mode to `RESPONSE_STREAM` +5. Verify the function works: `curl -N `. The results should be + streamed back with 0.5 second pause between each word. + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` diff --git a/examples/http-axum-streaming/src/main.rs b/examples/http-axum-streaming/src/main.rs new file mode 100644 index 000000000..1812f8795 --- /dev/null +++ b/examples/http-axum-streaming/src/main.rs @@ -0,0 +1,70 @@ +//! # Example: Axum Streaming Responses on AWS Lambda +//! +//! Demonstrates serving **incremental streaming responses** from Axum handlers +//! running in AWS Lambda. +//! +//! - Runs with `run_with_streaming_response`, which uses the **default Lambda +//! runtime** to convert Axum responses into streaming bodies delivered as +//! data is produced (unlike the OTel example, which used a custom `Runtime` + +//! `StreamAdapter`). + +use axum::{ + body::Body, + http::{ + self, + header::{CACHE_CONTROL, CONTENT_TYPE}, + StatusCode, + }, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use bytes::Bytes; +use core::{convert::Infallible, time::Duration}; +use lambda_http::{run_with_streaming_response, tracing, Error}; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("{0}")] + Http(#[from] http::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } +} + +async fn stream_words() -> Result { + let (tx, rx) = mpsc::channel::>(8); + let body = Body::from_stream(ReceiverStream::new(rx)); + + tokio::spawn(async move { + for msg in ["Hello", "world", "from", "Lambda!"] { + tokio::time::sleep(Duration::from_millis(500)).await; + if tx.send(Ok(Bytes::from(format!("{msg}\n")))).await.is_err() { + break; + } + } + }); + + Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/plain; charset=utf-8") + .header(CACHE_CONTROL, "no-cache") + .body(body)?) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let svc = Router::new().route("/", get(stream_words)); + + // Automatically convert the service into a streaming response with a + // default runtime. + run_with_streaming_response(svc).await +} diff --git a/examples/http-axum/Cargo.toml b/examples/http-axum/Cargo.toml index 7ab3c0ec4..7664e7a79 100644 --- a/examples/http-axum/Cargo.toml +++ b/examples/http-axum/Cargo.toml @@ -4,9 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.7" +axum = "0.8" lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.196" +serde = "1.0.219" serde_json = "1.0" tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-axum/cdk/package-lock.json b/examples/http-axum/cdk/package-lock.json index 9a7e30267..4b8ceb1f6 100644 --- a/examples/http-axum/cdk/package-lock.json +++ b/examples/http-axum/cdk/package-lock.json @@ -8,8 +8,8 @@ "name": "cdk", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "^2.126.0", - "cargo-lambda-cdk": "^0.0.17", + "aws-cdk-lib": "^2.262.0", + "cargo-lambda-cdk": "^0.0.35", "constructs": "^10.0.0", "source-map-support": "^0.5.21" }, @@ -40,19 +40,49 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.202", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", - "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" + "version": "2.2.282", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz", + "integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==" }, - "node_modules/@aws-cdk/asset-kubectl-v20": { + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", + "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==" }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", - "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "54.14.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.14.0.tgz", + "integrity": "sha512-JCZCzgp3SuXQVljaKqXnttHzcezEHt9Ag/YipK0XwUFD+Iz2T4jY7gUc3pA25Uq6pzY2n9DvO/nEU++dPXW4Rw==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "^1.5.0", + "semver": "^7.8.5" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.8.5", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@babel/code-frame": { "version": "7.23.5", @@ -1315,11 +1345,13 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.126.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.126.0.tgz", - "integrity": "sha512-HlwBYiqNCfhPUyozjoV4zJIBgH0adE/zTVATmeaM1jcNGs6jwkPMpWsF75JuXOoVtkcfpBFyNKloGd82nTiB0A==", + "version": "2.262.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz", + "integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==", "bundleDependencies": [ + "@aws/cloudformation-validate", "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", "case", "fs-extra", "ignore", @@ -1327,93 +1359,79 @@ "minimatch", "punycode", "semver", - "table", - "yaml" + "yaml", + "mime-types" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", + "@aws-cdk/asset-awscli-v1": "2.2.282", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.5.0-beta", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", - "ignore": "^5.3.0", - "jsonschema": "^1.4.1", - "minimatch": "^3.1.2", + "fs-extra": "^11.3.6", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.5", "punycode": "^2.3.1", - "semver": "^7.5.4", - "table": "^6.8.1", - "yaml": "1.10.2" + "semver": "^7.8.5", + "yaml": "1.10.3" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 20.0.0" }, "peerDependencies": { - "constructs": "^10.0.0" + "constructs": "^10.5.0" } }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.12.0", + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.6", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "jsonschema": "^1.5.0", + "semver": "^7.8.4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=54.5.0" } }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": { + "version": "1.5.0-beta", "inBundle": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^22.15.0", + "npm": ">=10.5.0" } }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "Apache-2.0" }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", "inBundle": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "1.1.11", + "version": "5.0.7", "inBundle": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/aws-cdk-lib/node_modules/case": { @@ -1424,39 +1442,8 @@ "node": ">= 0.8.0" } }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/concat-map": { - "version": "0.0.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.2.0", + "version": "11.3.6", "inBundle": true, "license": "MIT", "dependencies": { @@ -1474,28 +1461,15 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.0", + "version": "5.3.2", "inBundle": true, "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.1.0", + "version": "6.2.1", "inBundle": true, "license": "MIT", "dependencies": { @@ -1506,38 +1480,44 @@ } }, "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "inBundle": true, "license": "MIT", "engines": { "node": "*" } }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", "inBundle": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/aws-cdk-lib/node_modules/lru-cache": { - "version": "6.0.0", + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", "inBundle": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "3.1.2", + "version": "10.2.5", "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/aws-cdk-lib/node_modules/punycode": { @@ -1548,21 +1528,10 @@ "node": ">=6" } }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.4", + "version": "7.8.5", "inBundle": true, "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -1570,61 +1539,6 @@ "node": ">=10" } }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.8.1", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/aws-cdk-lib/node_modules/universalify": { "version": "2.0.1", "inBundle": true, @@ -1633,21 +1547,8 @@ "node": ">= 10.0.0" } }, - "node_modules/aws-cdk-lib/node_modules/uri-js": { - "version": "4.4.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.2", + "version": "1.10.3", "inBundle": true, "license": "ISC", "engines": { @@ -1886,22 +1787,22 @@ ] }, "node_modules/cargo-lambda-cdk": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.17.tgz", - "integrity": "sha512-KV95oV4GdczNioji8EvHbYumsArdGRusXsNhfRSBIoE94WN8jlxySaUzboNiH+HEKshyUQtnvdQd/WLBdk5NDA==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", "bundleDependencies": [ "js-toml" ], "dependencies": { - "js-toml": "^0.1.1" + "js-toml": "^1.0.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.1.0", + "aws-cdk-lib": "^2.215.0", "constructs": "^10.0.5" } }, "node_modules/cargo-lambda-cdk/node_modules/@babel/runtime-corejs3": { - "version": "7.22.15", + "version": "7.26.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -1913,49 +1814,54 @@ } }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/gast": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, + "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "inBundle": true, + "license": "Apache-2.0" + }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/types": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0" }, "node_modules/cargo-lambda-cdk/node_modules/@chevrotain/utils": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0" }, "node_modules/cargo-lambda-cdk/node_modules/chevrotain": { - "version": "10.5.0", + "version": "11.0.3", "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" } }, "node_modules/cargo-lambda-cdk/node_modules/core-js-pure": { - "version": "3.32.2", + "version": "3.39.0", "hasInstallScript": true, "inBundle": true, "license": "MIT", @@ -1965,26 +1871,21 @@ } }, "node_modules/cargo-lambda-cdk/node_modules/js-toml": { - "version": "0.1.1", + "version": "1.0.2", "inBundle": true, "license": "MIT", "dependencies": { - "chevrotain": "^10.4.1", + "chevrotain": "^11.0.3", "xregexp": "^5.1.1" } }, - "node_modules/cargo-lambda-cdk/node_modules/lodash": { + "node_modules/cargo-lambda-cdk/node_modules/lodash-es": { "version": "4.17.21", "inBundle": true, "license": "MIT" }, "node_modules/cargo-lambda-cdk/node_modules/regenerator-runtime": { - "version": "0.14.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/cargo-lambda-cdk/node_modules/regexp-to-ast": { - "version": "0.5.0", + "version": "0.14.1", "inBundle": true, "license": "MIT" }, @@ -2097,12 +1998,9 @@ "dev": true }, "node_modules/constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==", - "engines": { - "node": ">= 16.14.0" - } + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -3320,9 +3218,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -3505,9 +3403,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -3694,9 +3592,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "engines": { "node": ">=8.6" @@ -4408,19 +4306,33 @@ } }, "@aws-cdk/asset-awscli-v1": { - "version": "2.2.202", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", - "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" + "version": "2.2.282", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz", + "integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==" }, - "@aws-cdk/asset-kubectl-v20": { + "@aws-cdk/asset-node-proxy-agent-v6": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", - "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", + "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==" }, - "@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", - "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" + "@aws-cdk/cloud-assembly-schema": { + "version": "54.14.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.14.0.tgz", + "integrity": "sha512-JCZCzgp3SuXQVljaKqXnttHzcezEHt9Ag/YipK0XwUFD+Iz2T4jY7gUc3pA25Uq6pzY2n9DvO/nEU++dPXW4Rw==", + "requires": { + "jsonschema": "^1.5.0", + "semver": "^7.8.5" + }, + "dependencies": { + "jsonschema": { + "version": "1.5.0", + "bundled": true + }, + "semver": { + "version": "7.8.5", + "bundled": true + } + } }, "@babel/code-frame": { "version": "7.23.5", @@ -5423,95 +5335,60 @@ } }, "aws-cdk-lib": { - "version": "2.126.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.126.0.tgz", - "integrity": "sha512-HlwBYiqNCfhPUyozjoV4zJIBgH0adE/zTVATmeaM1jcNGs6jwkPMpWsF75JuXOoVtkcfpBFyNKloGd82nTiB0A==", - "requires": { - "@aws-cdk/asset-awscli-v1": "^2.2.202", - "@aws-cdk/asset-kubectl-v20": "^2.1.2", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", + "version": "2.262.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz", + "integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==", + "requires": { + "@aws-cdk/asset-awscli-v1": "2.2.282", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.5.0-beta", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.2.0", - "ignore": "^5.3.0", - "jsonschema": "^1.4.1", - "minimatch": "^3.1.2", + "fs-extra": "^11.3.6", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.5", "punycode": "^2.3.1", - "semver": "^7.5.4", - "table": "^6.8.1", - "yaml": "1.10.2" + "semver": "^7.8.5", + "yaml": "1.10.3" }, "dependencies": { - "@balena/dockerignore": { - "version": "1.0.2", - "bundled": true - }, - "ajv": { - "version": "8.12.0", + "@aws-cdk/cloud-assembly-api": { + "version": "2.2.6", "bundled": true, "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "jsonschema": "^1.5.0", + "semver": "^7.8.4" } }, - "ansi-regex": { - "version": "5.0.1", + "@aws/cloudformation-validate": { + "version": "1.5.0-beta", "bundled": true }, - "ansi-styles": { - "version": "4.3.0", - "bundled": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "astral-regex": { - "version": "2.0.0", + "@balena/dockerignore": { + "version": "1.0.2", "bundled": true }, "balanced-match": { - "version": "1.0.2", + "version": "4.0.4", "bundled": true }, "brace-expansion": { - "version": "1.1.11", + "version": "5.0.7", "bundled": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" } }, "case": { "version": "1.6.3", "bundled": true }, - "color-convert": { - "version": "2.0.1", - "bundled": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "bundled": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "emoji-regex": { - "version": "8.0.0", - "bundled": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "bundled": true - }, "fs-extra": { - "version": "11.2.0", + "version": "11.3.6", "bundled": true, "requires": { "graceful-fs": "^4.2.0", @@ -5524,19 +5401,11 @@ "bundled": true }, "ignore": { - "version": "5.3.0", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "bundled": true - }, - "json-schema-traverse": { - "version": "1.0.0", + "version": "5.3.2", "bundled": true }, "jsonfile": { - "version": "6.1.0", + "version": "6.2.1", "bundled": true, "requires": { "graceful-fs": "^4.1.6", @@ -5544,95 +5413,41 @@ } }, "jsonschema": { - "version": "1.4.1", + "version": "1.5.0", "bundled": true }, - "lodash.truncate": { - "version": "4.4.2", + "mime-db": { + "version": "1.52.0", "bundled": true }, - "lru-cache": { - "version": "6.0.0", + "mime-types": { + "version": "2.1.35", "bundled": true, "requires": { - "yallist": "^4.0.0" + "mime-db": "1.52.0" } }, "minimatch": { - "version": "3.1.2", + "version": "10.2.5", "bundled": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" } }, "punycode": { "version": "2.3.1", "bundled": true }, - "require-from-string": { - "version": "2.0.2", - "bundled": true - }, "semver": { - "version": "7.5.4", - "bundled": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "slice-ansi": { - "version": "4.0.0", - "bundled": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "bundled": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "bundled": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "table": { - "version": "6.8.1", - "bundled": true, - "requires": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - } + "version": "7.8.5", + "bundled": true }, "universalify": { "version": "2.0.1", "bundled": true }, - "uri-js": { - "version": "4.4.1", - "bundled": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "yallist": { - "version": "4.0.0", - "bundled": true - }, "yaml": { - "version": "1.10.2", + "version": "1.10.3", "bundled": true } } @@ -5801,15 +5616,15 @@ "dev": true }, "cargo-lambda-cdk": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.17.tgz", - "integrity": "sha512-KV95oV4GdczNioji8EvHbYumsArdGRusXsNhfRSBIoE94WN8jlxySaUzboNiH+HEKshyUQtnvdQd/WLBdk5NDA==", + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/cargo-lambda-cdk/-/cargo-lambda-cdk-0.0.35.tgz", + "integrity": "sha512-989zAeN/SpXhE1BmjtCuLmTX6nC9qm4CmlVFFJO6ob368w9dZBamlI3jouc+NAX+yW8vbYOih8LUkUIXCWF6VA==", "requires": { - "js-toml": "^0.1.1" + "js-toml": "^1.0.2" }, "dependencies": { "@babel/runtime-corejs3": { - "version": "7.22.15", + "version": "7.26.0", "bundled": true, "requires": { "core-js-pure": "^3.30.2", @@ -5817,64 +5632,64 @@ } }, "@chevrotain/cst-dts-gen": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, "@chevrotain/gast": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" } }, + "@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "bundled": true + }, "@chevrotain/types": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true }, "@chevrotain/utils": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true }, "chevrotain": { - "version": "10.5.0", + "version": "11.0.3", "bundled": true, "requires": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" } }, "core-js-pure": { - "version": "3.32.2", + "version": "3.39.0", "bundled": true }, "js-toml": { - "version": "0.1.1", + "version": "1.0.2", "bundled": true, "requires": { - "chevrotain": "^10.4.1", + "chevrotain": "^11.0.3", "xregexp": "^5.1.1" } }, - "lodash": { + "lodash-es": { "version": "4.17.21", "bundled": true }, "regenerator-runtime": { - "version": "0.14.0", - "bundled": true - }, - "regexp-to-ast": { - "version": "0.5.0", + "version": "0.14.1", "bundled": true }, "xregexp": { @@ -5959,9 +5774,9 @@ "dev": true }, "constructs": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.3.0.tgz", - "integrity": "sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==" + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.5.1.tgz", + "integrity": "sha512-f/TfFXiS3G/yVIXDjOQn9oTlyu9Wo7Fxyjj7lb8r92iO81jR2uST+9MstxZTmDGx/CgIbxCXkFXgupnLTNxQZg==" }, "convert-source-map": { "version": "2.0.0", @@ -6877,9 +6692,9 @@ "dev": true }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -7019,9 +6834,9 @@ "dev": true }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -7162,9 +6977,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "pirates": { diff --git a/examples/http-axum/cdk/package.json b/examples/http-axum/cdk/package.json index 9117d19fc..f4ce86a26 100644 --- a/examples/http-axum/cdk/package.json +++ b/examples/http-axum/cdk/package.json @@ -20,8 +20,8 @@ "typescript": "~5.3.3" }, "dependencies": { - "aws-cdk-lib": "^2.126.0", - "cargo-lambda-cdk": "^0.0.17", + "aws-cdk-lib": "^2.262.0", + "cargo-lambda-cdk": "^0.0.35", "constructs": "^10.0.0", "source-map-support": "^0.5.21" } diff --git a/examples/http-axum/src/main.rs b/examples/http-axum/src/main.rs index dcd5d1541..3b00fc57c 100644 --- a/examples/http-axum/src/main.rs +++ b/examples/http-axum/src/main.rs @@ -6,10 +6,9 @@ //! implementation to the Lambda runtime to run as a Lambda function. By using Axum instead //! of a basic `tower::Service` you get web framework niceties like routing, request component //! extraction, validation, etc. -use axum::extract::Query; -use axum::http::StatusCode; use axum::{ - extract::Path, + extract::{Path, Query}, + http::StatusCode, response::Json, routing::{get, post}, Router, diff --git a/examples/http-basic-lambda/Cargo.toml b/examples/http-basic-lambda/Cargo.toml index c7a515072..2f2523894 100644 --- a/examples/http-basic-lambda/Cargo.toml +++ b/examples/http-basic-lambda/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-basic-lambda/src/main.rs b/examples/http-basic-lambda/src/main.rs index d0e415613..87376d374 100644 --- a/examples/http-basic-lambda/src/main.rs +++ b/examples/http-basic-lambda/src/main.rs @@ -3,7 +3,7 @@ use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response}; /// This is the main body for the function. /// Write your code inside it. /// There are some code examples in the Runtime repository: -/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples +/// - async fn function_handler(_event: Request) -> Result, Error> { // Extract some useful information from the request diff --git a/examples/http-cors/Cargo.toml b/examples/http-cors/Cargo.toml index b8e510310..b9c9efa52 100644 --- a/examples/http-cors/Cargo.toml +++ b/examples/http-cors/Cargo.toml @@ -5,6 +5,5 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } -tower-http = { version = "0.5", features = ["cors"] } +tower-http = { version = "0.6", features = ["cors"] } diff --git a/examples/http-cors/src/main.rs b/examples/http-cors/src/main.rs index ffac0a9e8..4c2c54b4c 100644 --- a/examples/http-cors/src/main.rs +++ b/examples/http-cors/src/main.rs @@ -28,7 +28,7 @@ async fn func(event: Request) -> Result, Error> { .query_string_parameters_ref() .and_then(|params| params.first("first_name")) { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, + Some(first_name) => format!("Hello, {first_name}!").into_response().await, None => Response::builder() .status(400) .body("Empty first name".into()) diff --git a/examples/http-dynamodb/Cargo.toml b/examples/http-dynamodb/Cargo.toml index f2b8db98e..172b4ef1f 100644 --- a/examples/http-dynamodb/Cargo.toml +++ b/examples/http-dynamodb/Cargo.toml @@ -4,11 +4,10 @@ version = "0.1.0" edition = "2021" [dependencies] -simple-error = "0.3.0" -serde_json = "1.0.107" -serde = { version = "1.0.189", features = ["derive"] } -serde_dynamo = {version = "^4.2.7", features = ["aws-sdk-dynamodb+0_33"]} +serde_json = "1.0.140" +serde = { version = "1.0.219", features = ["derive"] } +serde_dynamo = { version = "4.2.14", features = ["aws-sdk-dynamodb+1"] } lambda_http = { path = "../../lambda-http" } -aws-sdk-dynamodb = "0.33.0" -aws-config = "0.56.1" -tokio = { version = "1.33.0", features = ["macros"] } +aws-sdk-dynamodb = "1.84.0" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +tokio = { version = "1.46.1", features = ["macros"] } diff --git a/examples/http-dynamodb/src/main.rs b/examples/http-dynamodb/src/main.rs index e5cbb2a34..006678a1c 100644 --- a/examples/http-dynamodb/src/main.rs +++ b/examples/http-dynamodb/src/main.rs @@ -1,21 +1,21 @@ use aws_sdk_dynamodb::Client; use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response}; use serde::{Deserialize, Serialize}; -use serde_dynamo::to_attribute_value; +use serde_dynamo::to_item; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Item { - pub p_type: String, + pub account_type: String, pub age: String, pub username: String, - pub first: String, - pub last: String, + pub first_name: String, + pub last_name: String, } /// This is the main body for the function. /// Write your code inside it. /// You can see more examples in Runtime's repository: -/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples +/// - async fn handle_request(db_client: &Client, event: Request) -> Result, Error> { // Extract some useful information from the request let body = event.body(); @@ -71,20 +71,9 @@ async fn main() -> Result<(), Error> { // Add an item to a table. // snippet-start:[dynamodb.rust.add-item] pub async fn add_item(client: &Client, item: Item, table: &str) -> Result<(), Error> { - let user_av = to_attribute_value(item.username)?; - let type_av = to_attribute_value(item.p_type)?; - let age_av = to_attribute_value(item.age)?; - let first_av = to_attribute_value(item.first)?; - let last_av = to_attribute_value(item.last)?; + let item = to_item(item)?; - let request = client - .put_item() - .table_name(table) - .item("username", user_av) - .item("account_type", type_av) - .item("age", age_av) - .item("first_name", first_av) - .item("last_name", last_av); + let request = client.put_item().table_name(table).set_item(Some(item)); tracing::info!("adding item to DynamoDB"); diff --git a/examples/http-query-parameters/Cargo.toml b/examples/http-query-parameters/Cargo.toml index 2cadda95d..18f8e6cfb 100644 --- a/examples/http-query-parameters/Cargo.toml +++ b/examples/http-query-parameters/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-query-parameters/src/main.rs b/examples/http-query-parameters/src/main.rs index 1f7110a5b..98893bea1 100644 --- a/examples/http-query-parameters/src/main.rs +++ b/examples/http-query-parameters/src/main.rs @@ -3,7 +3,7 @@ use lambda_http::{run, service_fn, tracing, Error, IntoResponse, Request, Reques /// This is the main body for the function. /// Write your code inside it. /// You can see more examples in Runtime's repository: -/// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples +/// - async fn function_handler(event: Request) -> Result { // Extract some useful information from the request Ok( @@ -11,7 +11,7 @@ async fn function_handler(event: Request) -> Result { .query_string_parameters_ref() .and_then(|params| params.first("first_name")) { - Some(first_name) => format!("Hello, {}!", first_name).into_response().await, + Some(first_name) => format!("Hello, {first_name}!").into_response().await, None => Response::builder() .status(400) .body("Empty first name".into()) diff --git a/examples/http-raw-path/Cargo.toml b/examples/http-raw-path/Cargo.toml index f6c56526d..d1c5ccb82 100644 --- a/examples/http-raw-path/Cargo.toml +++ b/examples/http-raw-path/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-shared-resource/Cargo.toml b/examples/http-shared-resource/Cargo.toml index 923ceecc1..8f5a0e947 100644 --- a/examples/http-shared-resource/Cargo.toml +++ b/examples/http-shared-resource/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = { path = "../../lambda-runtime" } tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-snapstart/Cargo.toml b/examples/http-snapstart/Cargo.toml new file mode 100644 index 000000000..a2bad8b88 --- /dev/null +++ b/examples/http-snapstart/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "http-snapstart" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_http = { path = "../../lambda-http" } +tokio = { version = "1", features = ["macros"] } diff --git a/examples/http-snapstart/Dockerfile b/examples/http-snapstart/Dockerfile new file mode 100644 index 000000000..0a72a338e --- /dev/null +++ b/examples/http-snapstart/Dockerfile @@ -0,0 +1,41 @@ +# Container image for the http-snapstart example. +# +# SnapStart for functions packaged as OCI images requires a custom base image +# whose runtime implements the restore lifecycle — which `lambda_runtime` now +# does. This image builds the example as the Lambda `bootstrap` on top of the +# AWS-provided base (`provided:al2023`), which supports SnapStart. +# +# Build from the REPOSITORY ROOT (the example uses path dependencies on the +# workspace crates, so the build context must include them): +# +# docker build -f examples/http-snapstart/Dockerfile -t http-snapstart . +# +# Then push to ECR and create the function with SnapStart enabled +# (PublishedVersions) and the architecture matching your build host. + +# ---- build stage ---- +FROM public.ecr.aws/lambda/provided:al2023 AS builder + +RUN dnf install -y gcc && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Copy only the workspace crates this example depends on via path, then the +# example itself: lambda_http -> lambda_runtime -> lambda_runtime_api_client, +# plus lambda-events (the aws_lambda_events crate used by lambda_http). +COPY Cargo.* /build/ +COPY lambda-http /build/lambda-http +COPY lambda-runtime /build/lambda-runtime +COPY lambda-runtime-api-client /build/lambda-runtime-api-client +COPY lambda-events /build/lambda-events +COPY examples/http-snapstart /build/examples/http-snapstart + +WORKDIR /build/examples/http-snapstart +RUN cargo build --release + +# ---- runtime stage ---- +FROM public.ecr.aws/lambda/provided:al2023 + +COPY --from=builder /build/examples/http-snapstart/target/release/http-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap + +ENTRYPOINT [ "/var/runtime/bootstrap" ] diff --git a/examples/http-snapstart/README.md b/examples/http-snapstart/README.md new file mode 100644 index 000000000..ecf459993 --- /dev/null +++ b/examples/http-snapstart/README.md @@ -0,0 +1,59 @@ +# AWS Lambda SnapStart example (lambda_http) + +This example shows how to use AWS Lambda **SnapStart** with the `lambda_http` +crate. It wraps a shared connection pool in a `SnapStartResource` and registers +it via the [`lambda_http::runtime`] helper, so the runtime drains the pool before +the VM snapshot and reconnects it after each restore. The HTTP handler keeps the +usual `lambda_http` request/response ergonomics. + +SnapStart reduces cold-start latency by snapshotting the initialized execution +environment and restoring from it. Resources created during init (connections, +credentials, unique values) may be invalid after restore — `before_snapshot` / +`after_restore` are where you release and re-establish them. When SnapStart is +not enabled, the hooks are never called and the function behaves normally. + +If you don't need custom hooks, plain `lambda_http::run(...)` already gets +SnapStart support for free: the runtime calls `/restore/next` and rebuilds its +internal RAPID connection pool on restore without any extra code. + +## Why a container image? + +SnapStart for functions packaged as OCI images requires a custom base image +whose runtime implements the restore lifecycle — which `lambda_runtime` (used by +`lambda_http`) now does. This example ships a `Dockerfile` that builds the binary +as the Lambda `bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`, +which supports SnapStart. + +## Build & Deploy + +Build the image from the **repository root** (the example depends on the +workspace crates via path, so the build context must include them): + +```sh +docker build -f examples/http-snapstart/Dockerfile -t http-snapstart . +``` + +Push it to ECR and create the function from the image: + +```sh +aws ecr create-repository --repository-name http-snapstart +docker tag http-snapstart:latest .dkr.ecr..amazonaws.com/http-snapstart:latest +docker push .dkr.ecr..amazonaws.com/http-snapstart:latest + +aws lambda create-function \ + --function-name http-snapstart \ + --package-type Image \ + --code ImageUri=.dkr.ecr..amazonaws.com/http-snapstart:latest \ + --role \ + --snap-start ApplyOn=PublishedVersions +``` + +SnapStart applies to **published versions**, so publish a version to trigger +snapshot creation, then expose it behind a function URL or API Gateway and invoke +it (e.g. `?name=world`). + +## Architecture + +The compiled `bootstrap` is architecture-specific. Build on (or for) the same +architecture as the target function — `provided:al2023` is available for both +`x86_64` and `arm64`. diff --git a/examples/http-snapstart/src/main.rs b/examples/http-snapstart/src/main.rs new file mode 100644 index 000000000..bcfffff61 --- /dev/null +++ b/examples/http-snapstart/src/main.rs @@ -0,0 +1,77 @@ +// This example demonstrates using SnapStart with the lambda_http crate. +// +// Use lambda_http::runtime() to get a Runtime on which you can register +// `SnapStartResource`s. This gives you access to the SnapStart lifecycle while +// keeping the lambda_http request/response ergonomics. +// +// For the simple case where no custom hooks are needed, just use +// lambda_http::run() — SnapStart works automatically. + +use lambda_http::{ + service_fn, tracing, BoxFuture, Body, Error, IntoResponse, Request, RequestExt, Response, SnapStartResource, +}; +use std::sync::Arc; +use tokio::sync::RwLock; + +struct DbPool { + connected: bool, +} + +impl DbPool { + async fn connect() -> Self { + tracing::info!("Establishing database connection pool"); + Self { connected: true } + } +} + +/// A `SnapStartResource` wrapper around the shared connection pool. The handler +/// and the resource share the same `Arc>`, so draining before the +/// snapshot and reconnecting after restore are visible to invocations. +struct PoolResource(Arc>); + +impl SnapStartResource for PoolResource { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Draining database connections before snapshot"); + self.0.write().await.connected = false; + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async move { + tracing::info!("Re-establishing database connections after restore"); + self.0.write().await.connected = true; + Ok(()) + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing::init_default_subscriber(); + + let pool = Arc::new(RwLock::new(DbPool::connect().await)); + + let pool_ref = pool.clone(); + let handler = service_fn(move |req: Request| { + let pool = pool_ref.clone(); + async move { + let pool = pool.read().await; + assert!(pool.connected, "Pool should be connected during invocation"); + + let name = req + .query_string_parameters_ref() + .and_then(|params| params.first("name")) + .unwrap_or("world"); + + Ok::, Error>(format!("Hello, {name}!").into_response().await) + } + }); + + lambda_http::runtime(handler) + .register_snapstart_resource(Arc::new(PoolResource(pool.clone()))) + .run() + .await?; + Ok(()) +} diff --git a/examples/http-tower-trace/Cargo.toml b/examples/http-tower-trace/Cargo.toml index 36389f0e5..cf1f223bb 100644 --- a/examples/http-tower-trace/Cargo.toml +++ b/examples/http-tower-trace/Cargo.toml @@ -5,6 +5,5 @@ edition = "2021" [dependencies] lambda_http = { path = "../../lambda-http" } -lambda_runtime = "0.5.1" tokio = { version = "1", features = ["macros"] } -tower-http = { version = "0.5", features = ["trace"] } +tower-http = { version = "0.6", features = ["trace"] } diff --git a/examples/http-tower-trace/src/main.rs b/examples/http-tower-trace/src/main.rs index df2b90073..7dd0c1d6a 100644 --- a/examples/http-tower-trace/src/main.rs +++ b/examples/http-tower-trace/src/main.rs @@ -1,6 +1,9 @@ -use lambda_http::tracing::{self, Level}; -use lambda_http::{run, tower::ServiceBuilder, Error}; -use lambda_http::{Request, Response}; +use lambda_http::{ + run, + tower::ServiceBuilder, + tracing::{self, Level}, + Error, Request, Response, +}; use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}; async fn handler(_req: Request) -> Result, Error> { diff --git a/examples/invocation-id-concurrent/Cargo.toml b/examples/invocation-id-concurrent/Cargo.toml new file mode 100644 index 000000000..3df86e683 --- /dev/null +++ b/examples/invocation-id-concurrent/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "invocation-id-concurrent" +version = "0.1.0" +edition = "2021" + +[dependencies] +lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] } +serde = "1" +tokio = { version = "1", features = ["macros", "rt", "time"] } + +[dev-dependencies] +serde_json = "1.0" diff --git a/examples/invocation-id-concurrent/src/main.rs b/examples/invocation-id-concurrent/src/main.rs new file mode 100644 index 000000000..acca09a05 --- /dev/null +++ b/examples/invocation-id-concurrent/src/main.rs @@ -0,0 +1,103 @@ +// This example requires the following input to succeed: +// { "command": "do something" } + +use lambda_runtime::{service_fn, tracing, Diagnostic, Error, LambdaEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +struct Request { + #[serde(default, rename = "command")] + _command: String, + sleep: u32, +} + +#[derive(Serialize, Debug, PartialEq)] +struct Response { + from: String, +} + +#[derive(Debug)] +struct HandlerError(String); + +impl std::fmt::Display for HandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Diagnostic { + fn from(e: HandlerError) -> Diagnostic { + Diagnostic { + error_type: "HandlerError".into(), + error_message: e.0, + } + } +} + + +/** + * Cross-wiring protection: duplicate request-id after timeout. + + Timeline: + t=0: Invoke A starts, handler sleeps 7s + t=5: A times out (timeout=5s). Batch 1 completes with timeout error. + t=5: Invoke B starts (same request-id), handler sleeps 4s + t=7: A's handler wakes up, posts stale /response/{same-id} + t=9: B's handler wakes up, posts correct /response/{same-id} + + With invocation-id: A's stale post at t=7 gets 410 Gone. B responds at t=9 correctly. + Without: A's stale response at t=7 is accepted for B (cross-wired). + */ + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing::init_default_subscriber(); + let max_concurrency = std::env::var("AWS_LAMBDA_MAX_CONCURRENCY").unwrap_or_else(|_| "not set".to_string()); + tracing::info!(AWS_LAMBDA_MAX_CONCURRENCY = %max_concurrency, "starting concurrent handler"); + + let func = service_fn(my_handler); + if let Err(err) = lambda_runtime::run_concurrent(func).await { + tracing::error!(error = %err, "run error"); + return Err(err); + } + Ok(()) +} + +pub(crate) async fn my_handler(event: LambdaEvent) -> Result { + if event.payload.sleep > 0 { + tokio::time::sleep(tokio::time::Duration::from_secs(event.payload.sleep.into())).await; + } + + Ok(Response { + from: event.payload._command, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use lambda_runtime::{Context, LambdaEvent}; + + #[tokio::test] + async fn handler_echoes_marker() { + let event = LambdaEvent { + payload: Request { + _command: "invoke-B".into(), + sleep: 0, + }, + context: Context::default(), + }; + + let result = my_handler(event).await.unwrap(); + + assert_eq!(result, Response { from: "invoke-B".into() }); + } + + #[test] + fn request_defaults_missing_command() { + let request: Request = serde_json::from_str(r#"{"sleep": 0}"#).unwrap(); + + assert_eq!(request._command, ""); + } +} diff --git a/examples/lambda-rds-iam-auth/Cargo.toml b/examples/lambda-rds-iam-auth/Cargo.toml index a1e212ae2..68b0fe929 100644 --- a/examples/lambda-rds-iam-auth/Cargo.toml +++ b/examples/lambda-rds-iam-auth/Cargo.toml @@ -5,10 +5,10 @@ edition = "2021" [dependencies] lambda_runtime = { path = "../../lambda-runtime" } -serde_json = "1.0.120" -aws-config = "1.0.1" -aws-credential-types = "1.0.1" -aws-sigv4 = "1.0.1" -url = "2.5.0" -tokio = { version = "1.25.0", features = ["full"] } -sqlx = { version = "0.7.4", features = ["tls-rustls", "postgres", "runtime-tokio"] } +serde_json = "1.0.140" +aws-config = { version = "1.8.1", features = ["behavior-version-latest"] } +aws-credential-types = "1.2.3" +aws-sigv4 = "1.3.3" +url = "2.5.4" +tokio = { version = "1.46.1", features = ["full"] } +sqlx = { version = "0.8.6", features = ["tls-rustls", "postgres", "runtime-tokio"] } diff --git a/examples/lambda-rds-iam-auth/cdk/cdk.json b/examples/lambda-rds-iam-auth/cdk/cdk.json deleted file mode 100644 index 8d92ca81b..000000000 --- a/examples/lambda-rds-iam-auth/cdk/cdk.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts app.ts", - "watch": { - "include": [ - "**.js", - "**.rs", - "**.ts" - ], - "exclude": [ - "README.md", - "cdk*.json", - "**/*.d.ts", - "**/*.js", - "tsconfig.json", - "package*.json", - "yarn.lock", - "node_modules", - "test" - ] - } -} \ No newline at end of file diff --git a/examples/lambda-rds-iam-auth/src/main.rs b/examples/lambda-rds-iam-auth/src/main.rs index 32cf35803..fbf8394f3 100644 --- a/examples/lambda-rds-iam-auth/src/main.rs +++ b/examples/lambda-rds-iam-auth/src/main.rs @@ -7,17 +7,15 @@ use aws_sigv4::{ use lambda_runtime::{run, service_fn, Error, LambdaEvent}; use serde_json::{json, Value}; use sqlx::postgres::PgConnectOptions; -use std::env; -use std::time::{Duration, SystemTime}; +use std::{ + env, + time::{Duration, SystemTime}, +}; const RDS_CERTS: &[u8] = include_bytes!("global-bundle.pem"); -async fn generate_rds_iam_token( - db_hostname: &str, - port: u16, - db_username: &str, -) -> Result { - let config = aws_config::load_defaults(BehaviorVersion::v2024_03_28()).await; +async fn generate_rds_iam_token(db_hostname: &str, port: u16, db_username: &str) -> Result { + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; let credentials = config .credentials_provider() @@ -40,23 +38,16 @@ async fn generate_rds_iam_token( .settings(signing_settings) .build()?; - let url = format!( - "https://{db_hostname}:{port}/?Action=connect&DBUser={db_user}", - db_hostname = db_hostname, - port = port, - db_user = db_username - ); + let url = format!("https://{db_hostname}:{port}/?Action=connect&DBUser={db_username}"); let signable_request = - SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::Bytes(&[])) - .expect("signable request"); + SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::Bytes(&[])).expect("signable request"); - let (signing_instructions, _signature) = - sign(signable_request, &signing_params.into())?.into_parts(); + let (signing_instructions, _signature) = sign(signable_request, &signing_params.into())?.into_parts(); let mut url = url::Url::parse(&url).unwrap(); for (name, value) in signing_instructions.params() { - url.query_pairs_mut().append_pair(name, &value); + url.query_pairs_mut().append_pair(name, value); } let response = url.to_string().split_off("https://".len()); @@ -89,9 +80,7 @@ async fn handler(_event: LambdaEvent) -> Result { .ssl_root_cert_from_pem(RDS_CERTS.to_vec()) .ssl_mode(sqlx::postgres::PgSslMode::Require); - let pool = sqlx::postgres::PgPoolOptions::new() - .connect_with(opts) - .await?; + let pool = sqlx::postgres::PgPoolOptions::new().connect_with(opts).await?; let result: i32 = sqlx::query_scalar("SELECT $1 + $2") .bind(3) @@ -99,7 +88,7 @@ async fn handler(_event: LambdaEvent) -> Result { .fetch_one(&pool) .await?; - println!("Result: {:?}", result); + println!("Result: {result:?}"); Ok(json!({ "statusCode": 200, diff --git a/examples/opentelemetry-tracing/Cargo.toml b/examples/opentelemetry-tracing/Cargo.toml index 98108d396..c9b2a9cb2 100644 --- a/examples/opentelemetry-tracing/Cargo.toml +++ b/examples/opentelemetry-tracing/Cargo.toml @@ -5,14 +5,12 @@ edition = "2021" [dependencies] lambda_runtime = { path = "../../lambda-runtime", features = ["opentelemetry"] } -opentelemetry-semantic-conventions = "0.14" -opentelemetry = "0.22" -opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] } -opentelemetry-stdout = { version = "0.3", features = ["trace"] } -pin-project = "1" +opentelemetry = "0.30" +opentelemetry_sdk = { version = "0.30", features = ["rt-tokio"] } +opentelemetry-stdout = { version = "0.30", features = ["trace"] } serde_json = "1.0" tokio = "1" tower = "0.5" tracing = "0.1" -tracing-opentelemetry = "0.23" +tracing-opentelemetry = "0.31" tracing-subscriber = "0.3" diff --git a/examples/opentelemetry-tracing/src/main.rs b/examples/opentelemetry-tracing/src/main.rs index 062f5a110..a75aa0168 100644 --- a/examples/opentelemetry-tracing/src/main.rs +++ b/examples/opentelemetry-tracing/src/main.rs @@ -4,7 +4,7 @@ use lambda_runtime::{ LambdaEvent, Runtime, }; use opentelemetry::trace::TracerProvider; -use opentelemetry_sdk::{runtime, trace}; +use opentelemetry_sdk::trace; use tower::{service_fn, BoxError}; use tracing_subscriber::prelude::*; @@ -18,8 +18,8 @@ async fn echo(event: LambdaEvent) -> Result Result<(), BoxError> { // Set up OpenTelemetry tracer provider that writes spans to stdout for debugging purposes let exporter = opentelemetry_stdout::SpanExporter::default(); - let tracer_provider = trace::TracerProvider::builder() - .with_batch_exporter(exporter, runtime::Tokio) + let tracer_provider = trace::SdkTracerProvider::builder() + .with_batch_exporter(exporter) .build(); // Set up link between OpenTelemetry and tracing crate @@ -34,7 +34,9 @@ async fn main() -> Result<(), BoxError> { // Create a tracing span for each Lambda invocation OtelLayer::new(|| { // Make sure that the trace is exported before the Lambda runtime is frozen - tracer_provider.force_flush(); + if let Err(err) = tracer_provider.force_flush() { + eprintln!("Error flushing traces: {err:#?}"); + } }) // Set the "faas.trigger" attribute of the span to "pubsub" .with_trigger(OpenTelemetryFaasTrigger::PubSub), diff --git a/lambda-events/CHANGELOG.md b/lambda-events/CHANGELOG.md new file mode 100644 index 000000000..61fefd7d8 --- /dev/null +++ b/lambda-events/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.2.0...aws_lambda_events-v1.2.1) - 2026-09-29 + +### Other + +- Add #[serde(default)] to `to` field of SimpleEmailCommonHeaders ([#1171](https://github.com/aws/aws-lambda-rust-runtime/pull/1171)) + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.3...aws_lambda_events-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.2...aws_lambda_events-v1.1.3) - 2026-04-16 + +### Other + +- remove non exhaustive from time structs ([#1138](https://github.com/aws/aws-lambda-rust-runtime/pull/1138)) + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.1.1...aws_lambda_events-v1.1.2) - 2026-03-19 + +### Fixed + +- fix handling of null groupConfiguration ([#1130](https://github.com/aws/aws-lambda-rust-runtime/pull/1130)) + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.0.3...aws_lambda_events-v1.1.1) - 2026-03-11 + +### Added + +- *(lambda-events)* support X509 custom authorizer in IoT events ([#1114](https://github.com/aws/aws-lambda-rust-runtime/pull/1114)) +- *(lambda-events)* add Control Tower lifecycle events module ([#1107](https://github.com/aws/aws-lambda-rust-runtime/pull/1107)) +- *(lambda-events)* add VPC Lattice event structures ([#1036](https://github.com/aws/aws-lambda-rust-runtime/pull/1036)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Fixed + +- *(sns)* support SubscriptionConfirmation and UnsubscribeConfirmation message types ([#1102](https://github.com/aws/aws-lambda-rust-runtime/pull/1102)) +- *(lambda-events)* `claims_to_add_or_override` type in `CognitoEventUserPoolsPreTokenGenV2` ([#1100](https://github.com/aws/aws-lambda-rust-runtime/pull/1100)) + +### Other + +- *(lambda-events)* deprecate authorizer-specific fields in `ApiGatewayV2httpRequest` ([#1089](https://github.com/aws/aws-lambda-rust-runtime/pull/1089)) + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/aws_lambda_events-v1.0.2...aws_lambda_events-v1.0.3) - 2026-01-06 + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-events/Cargo.toml b/lambda-events/Cargo.toml index b8d34055e..f20987022 100644 --- a/lambda-events/Cargo.toml +++ b/lambda-events/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "aws_lambda_events" -version = "0.16.0" +version = "1.2.1" +rust-version = "1.84.0" description = "AWS Lambda event definitions" authors = [ "Christian Legnitto ", @@ -8,8 +9,8 @@ authors = [ "David Calavera ", ] license = "MIT" -homepage = "https://github.com/awslabs/aws-lambda-rust-runtime" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +homepage = "https://github.com/aws/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" readme = "README.md" keywords = ["lambda", "aws", "amazon", "events", "S3"] categories = ["api-bindings", "encoding", "web-programming"] @@ -19,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 } @@ -77,6 +79,8 @@ default = [ "streams", "documentdb", "eventbridge", + "controltower", + "vpc_lattice" ] activemq = [] @@ -100,6 +104,7 @@ codepipeline_job = [] cognito = [] config = [] connect = [] +controltower = [] dynamodb = ["chrono", "serde_dynamo", "streams"] ecr_scan = [] firehose = ["chrono"] @@ -123,3 +128,13 @@ sqs = ["serde_with"] streams = [] documentdb = [] eventbridge = ["chrono", "serde_with"] +vpc_lattice = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"] + +catch-all-fields = [] +builders = ["bon"] + +[package.metadata.docs.rs] +all-features = true + +[dev-dependencies] +lambda_runtime = { version = "1", path = "../lambda-runtime" } diff --git a/lambda-events/README.md b/lambda-events/README.md index 8446ce554..c1166cb49 100644 --- a/lambda-events/README.md +++ b/lambda-events/README.md @@ -15,7 +15,7 @@ The crate itself has no AWS Lambda handler logic and instead exists to serialize and deserialize AWS Lambda events into strongly-typed Rust structs. The types -defined in this crate are usually used with handlers / runtimes provided by the [official Rust runtime](https://github.com/awslabs/aws-lambda-rust-runtime). +defined in this crate are usually used with handlers / runtimes provided by the [official Rust runtime](https://github.com/aws/aws-lambda-rust-runtime). For a list of supported AWS Lambda events and services, see [the crate reference documentation](https://docs.rs/aws_lambda_events). @@ -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, +} + +async fn handler( + event: LambdaEvent, +) -> Result, 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. diff --git a/lambda-events/lambda-events-examples/Cargo.toml b/lambda-events/lambda-events-examples/Cargo.toml new file mode 100644 index 000000000..216faa082 --- /dev/null +++ b/lambda-events/lambda-events-examples/Cargo.toml @@ -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" diff --git a/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs b/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs new file mode 100644 index 000000000..148594068 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/comprehensive-builders.rs @@ -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(); +} diff --git a/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs b/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs new file mode 100644 index 000000000..f92762c90 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/lambda-runtime-authorizer-builder.rs @@ -0,0 +1,96 @@ +// Example showing how builders work with generic types and custom context structs +// +// Demonstrates: +// 1. Generic types (ApiGatewayV2CustomAuthorizerSimpleResponse) +// 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, +} + +// 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, +) -> Result, 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, +) -> Result, 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); +} diff --git a/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs b/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs new file mode 100644 index 000000000..7330402d0 --- /dev/null +++ b/lambda-events/lambda-events-examples/examples/lambda-runtime-sqs-batch-builder.rs @@ -0,0 +1,64 @@ +// Example showing how builders simplify SQS batch response construction +// when handling partial batch failures + +use aws_lambda_events::event::sqs::{BatchItemFailure, SqsBatchResponse, SqsEvent}; +use lambda_runtime::{Error, LambdaEvent}; + +// Simulate processing a record +#[allow(dead_code)] +async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), String> { + // Simulate some processing logic + if let Some(body) = &record.body { + if body.contains("error") { + return Err(format!("Failed to process message: {}", body)); + } + } + Ok(()) +} + +// Lambda handler using builder pattern +#[allow(dead_code)] +pub async fn function_handler(event: LambdaEvent) -> Result { + let mut batch_item_failures = Vec::new(); + + for record in event.payload.records { + match process_record(&record).await { + Ok(_) => (), + Err(_) => { + let item = BatchItemFailure::builder() + .item_identifier(record.message_id.unwrap()) + .build(); + + batch_item_failures.push(item) + } + } + } + + let response = SqsBatchResponse::builder() + .batch_item_failures(batch_item_failures) + .build(); + + Ok(response) +} + +fn main() { + // Demonstrate builder usage with sample data + let failures = vec![ + BatchItemFailure::builder() + .item_identifier("msg-123".to_string()) + .build(), + BatchItemFailure::builder() + .item_identifier("msg-456".to_string()) + .build(), + ]; + + let response = SqsBatchResponse::builder().batch_item_failures(failures).build(); + + println!( + "Built SQS batch response with {} failed items", + response.batch_item_failures.len() + ); + for failure in &response.batch_item_failures { + println!(" Failed message: {}", failure.item_identifier); + } +} diff --git a/lambda-events/src/custom_serde/codebuild_time.rs b/lambda-events/src/custom_serde/codebuild_time.rs index 92b0f7961..8d90203fc 100644 --- a/lambda-events/src/custom_serde/codebuild_time.rs +++ b/lambda-events/src/custom_serde/codebuild_time.rs @@ -14,13 +14,13 @@ impl Visitor<'_> for TimeVisitor { type Value = DateTime; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "valid codebuild time: {}", CODEBUILD_TIME_FORMAT) + write!(formatter, "valid codebuild time: {CODEBUILD_TIME_FORMAT}") } fn visit_str(self, val: &str) -> Result { NaiveDateTime::parse_from_str(val, CODEBUILD_TIME_FORMAT) .map(|naive| naive.and_utc()) - .map_err(|e| DeError::custom(format!("Parse error {} for {}", e, val))) + .map_err(|e| DeError::custom(format!("Parse error {e} for {val}"))) } } diff --git a/lambda-events/src/custom_serde/float_unix_epoch.rs b/lambda-events/src/custom_serde/float_unix_epoch.rs index 805c672f3..164b4c7e6 100644 --- a/lambda-events/src/custom_serde/float_unix_epoch.rs +++ b/lambda-events/src/custom_serde/float_unix_epoch.rs @@ -14,7 +14,7 @@ fn ne_timestamp(ts: T) -> SerdeError { impl fmt::Debug for SerdeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "ChronoSerdeError({})", self) + write!(f, "ChronoSerdeError({self})") } } @@ -22,7 +22,7 @@ impl fmt::Display for SerdeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { SerdeError::NonExistent { ref timestamp } => { - write!(f, "value is not a legal timestamp: {}", timestamp) + write!(f, "value is not a legal timestamp: {timestamp}") } SerdeError::Ambiguous { ref timestamp, @@ -30,8 +30,7 @@ impl fmt::Display for SerdeError { ref max, } => write!( f, - "value is an ambiguous timestamp: {}, could be either of {}, {}", - timestamp, min, max + "value is an ambiguous timestamp: {timestamp}, could be either of {min}, {max}", ), } } @@ -57,7 +56,6 @@ where struct SecondsFloatTimestampVisitor; /// Serialize a UTC datetime into an float number of seconds since the epoch -/// ``` pub fn serialize(dt: &DateTime, serializer: S) -> Result where S: ser::Serializer, diff --git a/lambda-events/src/custom_serde/headers.rs b/lambda-events/src/custom_serde/headers.rs index 448846498..b343180df 100644 --- a/lambda-events/src/custom_serde/headers.rs +++ b/lambda-events/src/custom_serde/headers.rs @@ -5,7 +5,7 @@ use serde::{ }; use std::{borrow::Cow, fmt}; -/// Serialize a http::HeaderMap into a serde str => Vec map +/// Serialize a http::HeaderMap into a serde str => `Vec` map pub(crate) fn serialize_multi_value_headers(headers: &HeaderMap, serializer: S) -> Result where S: Serializer, @@ -34,6 +34,24 @@ where map.end() } +/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings. +/// Used by VPC Lattice V1 which expects multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn serialize_comma_separated_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + for key in headers.keys() { + let values: Vec<&str> = headers.get_all(key).iter().filter_map(|v| v.to_str().ok()).collect(); + if !values.is_empty() { + let combined_value = values.join(", "); + map.serialize_entry(key.as_str(), &combined_value)?; + } + } + map.end() +} + #[derive(serde::Deserialize)] #[serde(untagged)] enum OneOrMore<'a> { @@ -44,6 +62,7 @@ enum OneOrMore<'a> { struct HeaderMapVisitor { is_human_readable: bool, + split_comma_separated: bool, } impl<'de> Visitor<'de> for HeaderMapVisitor { @@ -97,17 +116,25 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; match val { OneOrMore::One(val) => { - let val = val - .parse() - .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; - map.insert(key, val); - } - OneOrMore::Strings(arr) => { - for val in arr { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { let val = val .parse() .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; - map.append(&key, val); + map.insert(key, val); + } + } + OneOrMore::Strings(arr) => { + for val in arr { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.append(&key, val); + } } } OneOrMore::Bytes(arr) => { @@ -124,13 +151,51 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { } } +fn split_and_append_header( + map: &mut HeaderMap, + key: &HeaderName, + value: &str, + visitor: &HeaderMapVisitor, +) -> Result<(), E> +where + E: DeError, +{ + for split_val in value.split(',') { + let trimmed_val = split_val.trim(); + if !trimmed_val.is_empty() { + let header_val = trimmed_val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?; + map.append(key, header_val); + } + } + Ok(()) +} + /// Implementation detail. pub(crate) fn deserialize_headers<'de, D>(de: D) -> Result where D: Deserializer<'de>, { let is_human_readable = de.is_human_readable(); - de.deserialize_option(HeaderMapVisitor { is_human_readable }) + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: false, + }) +} + +/// Deserialize headers, splitting comma-separated values into multiple header values. +/// Used by VPC Lattice V1 which sends multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let is_human_readable = de.is_human_readable(); + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: true, + }) } #[cfg(test)] diff --git a/lambda-events/src/custom_serde/mod.rs b/lambda-events/src/custom_serde/mod.rs index 729dee3dd..d7b340f27 100644 --- a/lambda-events/src/custom_serde/mod.rs +++ b/lambda-events/src/custom_serde/mod.rs @@ -3,11 +3,11 @@ use serde::{ de::{Deserialize, Deserializer, Error as DeError}, ser::Serializer, }; -use std::collections::HashMap; #[cfg(feature = "codebuild")] pub(crate) mod codebuild_time; #[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] pub type CodeBuildNumber = f32; #[cfg(any( @@ -15,7 +15,8 @@ pub type CodeBuildNumber = f32; feature = "apigw", feature = "s3", feature = "iot", - feature = "lambda_function_urls" + feature = "lambda_function_urls", + feature = "vpc_lattice" ))] mod headers; #[cfg(any( @@ -23,16 +24,22 @@ mod headers; feature = "apigw", feature = "s3", feature = "iot", - feature = "lambda_function_urls" + feature = "lambda_function_urls", + feature = "vpc_lattice" ))] pub(crate) use self::headers::*; #[cfg(feature = "dynamodb")] pub(crate) mod float_unix_epoch; -#[cfg(any(feature = "alb", feature = "apigw"))] +#[cfg(any(feature = "alb", feature = "apigw", feature = "vpc_lattice"))] pub(crate) mod http_method; +#[cfg(feature = "alb")] +mod query_string_parameters; +#[cfg(feature = "alb")] +pub(crate) use self::query_string_parameters::*; + pub(crate) fn deserialize_base64<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -50,46 +57,18 @@ where serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(value)) } -/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map. -pub(crate) fn deserialize_lambda_map<'de, D, K, V>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, - K: serde::Deserialize<'de>, - K: std::hash::Hash, - K: std::cmp::Eq, - V: serde::Deserialize<'de>, -{ - // https://github.com/serde-rs/serde/issues/1098 - let opt = Option::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} - -#[cfg(feature = "dynamodb")] -/// Deserializes `Item`, mapping JSON `null` to an empty item. -pub(crate) fn deserialize_lambda_dynamodb_item<'de, D>(deserializer: D) -> Result +/// Deserializes any `Default` type, mapping JSON `null` to `T::default()`. +/// +/// **Note** null-to-empty semantics are usually clear for container types (Map, Vec, etc). +/// For most other data types, prefer modeling fields as ```Option``` with #[serde(default)] +/// instead of using this deserializer. Option preserves information about the message +/// for the application, and default semantics for the target data type may change +/// over time without warning. +pub(crate) fn deserialize_nullish<'de, D, T>(deserializer: D) -> Result where D: Deserializer<'de>, + T: Default + Deserialize<'de>, { - // https://github.com/serde-rs/serde/issues/1098 - let opt = Option::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} - -/// Deserializes `HashMap<_>`, mapping JSON `null` to an empty map. -#[cfg(any( - feature = "alb", - feature = "apigw", - feature = "cloudwatch_events", - feature = "code_commit", - feature = "cognito", - feature = "sns", - test -))] -pub(crate) fn deserialize_nullish_boolean<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - // https://github.com/serde-rs/serde/issues/1098 let opt = Option::deserialize(deserializer)?; Ok(opt.unwrap_or_default()) } @@ -98,7 +77,9 @@ where #[allow(deprecated)] mod test { use super::*; + use serde::{Deserialize, Serialize}; + use std::collections::HashMap; #[test] fn test_deserialize_base64() { @@ -132,7 +113,7 @@ mod test { fn test_deserialize_map() { #[derive(Deserialize)] struct Test { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] v: HashMap, } let input = serde_json::json!({ @@ -151,9 +132,9 @@ mod test { #[cfg(feature = "dynamodb")] #[test] fn test_deserialize_lambda_dynamodb_item() { - #[derive(Deserialize)] + #[derive(Deserialize, Debug)] struct Test { - #[serde(deserialize_with = "deserialize_lambda_dynamodb_item")] + #[serde(deserialize_with = "deserialize_nullish")] v: serde_dynamo::Item, } let input = serde_json::json!({ @@ -167,13 +148,39 @@ mod test { }); let decoded: Test = serde_json::from_value(input).unwrap(); assert_eq!(serde_dynamo::Item::from(HashMap::new()), decoded.v); + + let input = serde_json::json!({}); + let failure = serde_json::from_value::(input); + assert!(failure.is_err(), "Missing field should not default: {failure:?}") + } + + #[test] + fn test_deserialize_nullish() { + #[derive(Debug, Default, Deserialize, PartialEq)] + struct Inner { + x: u32, + } + #[derive(Deserialize)] + struct Test { + #[serde(default, deserialize_with = "deserialize_nullish")] + v: Inner, + } + + let decoded: Test = serde_json::from_str(r#"{"v": null}"#).unwrap(); + assert_eq!(decoded.v, Inner::default()); + + let decoded: Test = serde_json::from_str(r#"{}"#).unwrap(); + assert_eq!(decoded.v, Inner::default()); + + let decoded: Test = serde_json::from_str(r#"{"v": {"x": 42}}"#).unwrap(); + assert_eq!(decoded.v, Inner { x: 42 }); } #[test] fn test_deserialize_nullish_boolean() { #[derive(Deserialize)] struct Test { - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] v: bool, } diff --git a/lambda-events/src/custom_serde/query_string_parameters.rs b/lambda-events/src/custom_serde/query_string_parameters.rs new file mode 100644 index 000000000..a7144edab --- /dev/null +++ b/lambda-events/src/custom_serde/query_string_parameters.rs @@ -0,0 +1,63 @@ +use query_map::QueryMap; +use serde::{ser::SerializeMap, Serializer}; +use std::collections::HashMap; + +/// Serializes `QueryMap`, converting value from `Vec` to `String` using the last value. +pub fn serialize_query_string_parameters(value: &QueryMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut query_string_parameters = HashMap::new(); + + if let Some((mut last_key, mut last_value)) = value.iter().next() { + // insert the last value for each key + value.iter().for_each(|(k, v)| { + if k != last_key { + query_string_parameters.insert(last_key, last_value); + last_key = k; + } + last_value = v; + }); + // insert the last pair + query_string_parameters.insert(last_key, last_value); + } + + let mut map = serializer.serialize_map(Some(query_string_parameters.len()))?; + for (k, v) in &query_string_parameters { + map.serialize_entry(k, v)?; + } + map.end() +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Serialize; + use serde_json::Value; + + #[test] + fn test_serialize_query_string_parameters() { + #[derive(Serialize)] + struct Test { + #[serde(serialize_with = "serialize_query_string_parameters")] + pub v: QueryMap, + } + + fn s(value: &str) -> String { + value.to_string() + } + + let query = QueryMap::from(HashMap::from([ + (s("key1"), vec![s("value1"), s("value2"), s("value3")]), + (s("key2"), vec![s("value4")]), + (s("key3"), vec![s("value5"), s("value6")]), + ])); + + let serialized = serde_json::to_string(&Test { v: query }).unwrap(); + let parsed: Value = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(parsed["v"]["key1"], Value::String("value3".to_string())); + assert_eq!(parsed["v"]["key2"], Value::String("value4".to_string())); + assert_eq!(parsed["v"]["key3"], Value::String("value6".to_string())); + } +} diff --git a/lambda-events/src/encodings/http.rs b/lambda-events/src/encodings/http.rs index d978f522a..32137a759 100644 --- a/lambda-events/src/encodings/http.rs +++ b/lambda-events/src/encodings/http.rs @@ -60,6 +60,7 @@ use std::{borrow::Cow, mem::take, ops::Deref, pin::Pin, task::Poll}; /// /// For more information about API Gateway's body types, /// refer to [this documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html). +#[non_exhaustive] #[derive(Debug, Default, Eq, PartialEq)] pub enum Body { /// An empty body @@ -264,7 +265,7 @@ mod tests { fn from_str() { match Body::from(String::from("foo").as_str()) { Body::Text(_) => (), - not => panic!("expected Body::Text(...) got {:?}", not), + not => panic!("expected Body::Text(...) got {not:?}"), } } @@ -272,7 +273,7 @@ mod tests { fn from_string() { match Body::from(String::from("foo")) { Body::Text(_) => (), - not => panic!("expected Body::Text(...) got {:?}", not), + not => panic!("expected Body::Text(...) got {not:?}"), } } @@ -280,7 +281,7 @@ mod tests { fn from_cow_str() { match Body::from(Cow::from("foo")) { Body::Text(_) => (), - not => panic!("expected Body::Text(...) got {:?}", not), + not => panic!("expected Body::Text(...) got {not:?}"), } } @@ -288,7 +289,7 @@ mod tests { fn from_cow_bytes() { match Body::from(Cow::from("foo".as_bytes())) { Body::Binary(_) => (), - not => panic!("expected Body::Binary(...) got {:?}", not), + not => panic!("expected Body::Binary(...) got {not:?}"), } } @@ -296,7 +297,7 @@ mod tests { fn from_bytes() { match Body::from("foo".as_bytes()) { Body::Binary(_) => (), - not => panic!("expected Body::Binary(...) got {:?}", not), + not => panic!("expected Body::Binary(...) got {not:?}"), } } @@ -325,12 +326,12 @@ mod tests { fn serialize_from_maybe_encoded() { match Body::from_maybe_encoded(false, "foo") { Body::Text(_) => (), - not => panic!("expected Body::Text(...) got {:?}", not), + not => panic!("expected Body::Text(...) got {not:?}"), } match Body::from_maybe_encoded(true, "Zm9v") { Body::Binary(b) => assert_eq!(&[102, 111, 111], b.as_slice()), - not => panic!("expected Body::Text(...) got {:?}", not), + not => panic!("expected Body::Text(...) got {not:?}"), } } } diff --git a/lambda-events/src/encodings/mod.rs b/lambda-events/src/encodings/mod.rs index 233996648..8b3089477 100644 --- a/lambda-events/src/encodings/mod.rs +++ b/lambda-events/src/encodings/mod.rs @@ -6,15 +6,18 @@ mod time; use crate::custom_serde::{deserialize_base64, serialize_base64}; #[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] pub use self::time::*; #[cfg(feature = "http")] mod http; #[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] pub use self::http::*; pub type Error = Box; /// Binary data encoded in base64. +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct Base64Data( #[serde(deserialize_with = "deserialize_base64")] diff --git a/lambda-events/src/encodings/time.rs b/lambda-events/src/encodings/time.rs index d49033608..c7ca04a61 100644 --- a/lambda-events/src/encodings/time.rs +++ b/lambda-events/src/encodings/time.rs @@ -7,7 +7,7 @@ use serde::{ use std::ops::{Deref, DerefMut}; /// Timestamp with millisecond precision. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct MillisecondTimestamp( #[serde(deserialize_with = "deserialize_milliseconds")] #[serde(serialize_with = "serialize_milliseconds")] @@ -29,7 +29,7 @@ impl DerefMut for MillisecondTimestamp { } /// Timestamp with second precision. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct SecondTimestamp( #[serde(deserialize_with = "deserialize_seconds")] #[serde(serialize_with = "serialize_seconds")] @@ -73,7 +73,7 @@ impl DerefMut for SecondDuration { } /// Duration with minute precision. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct MinuteDuration( #[serde(deserialize_with = "deserialize_duration_minutes")] #[serde(serialize_with = "serialize_duration_minutes")] @@ -125,7 +125,7 @@ where let whole_seconds = seconds + (milliseconds as i64 / 1000); let subsec_millis = milliseconds % 1000; if milliseconds > 0 { - let combined = format!("{}.{:03}", whole_seconds, subsec_millis); + let combined = format!("{whole_seconds}.{subsec_millis:03}"); serializer.serialize_str(&combined) } else { serializer.serialize_str(&whole_seconds.to_string()) @@ -159,7 +159,7 @@ where { let seconds = f64::deserialize(deserializer)?; TimeDelta::try_seconds(seconds as i64) - .ok_or_else(|| D::Error::custom(format!("invalid time delta seconds `{}`", seconds))) + .ok_or_else(|| D::Error::custom(format!("invalid time delta seconds `{seconds}`"))) } fn serialize_duration_minutes(duration: &TimeDelta, serializer: S) -> Result @@ -177,7 +177,7 @@ where { let minutes = f64::deserialize(deserializer)?; TimeDelta::try_minutes(minutes as i64) - .ok_or_else(|| D::Error::custom(format!("invalid time delta minutes `{}`", minutes))) + .ok_or_else(|| D::Error::custom(format!("invalid time delta minutes `{minutes}`"))) } fn normalize_timestamp<'de, D>(deserializer: D) -> Result<(u64, u64), D::Error> @@ -199,7 +199,7 @@ where }; // We need to do this due to floating point issues. - let input_as_string = format!("{}", input); + let input_as_string = input.to_string(); let parts: Result, _> = input_as_string .split('.') .map(|x| x.parse::().map_err(DeError::custom)) diff --git a/lambda-events/src/event/activemq/mod.rs b/lambda-events/src/event/activemq/mod.rs index 89cfda1c2..dcf297dac 100644 --- a/lambda-events/src/event/activemq/mod.rs +++ b/lambda-events/src/event/activemq/mod.rs @@ -1,9 +1,15 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActiveMqEvent { #[serde(default)] @@ -11,9 +17,19 @@ pub struct ActiveMqEvent { #[serde(default)] pub event_source_arn: Option, pub messages: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActiveMqMessage { #[serde(default)] @@ -38,29 +54,57 @@ pub struct ActiveMqMessage { pub data: Option, pub broker_in_time: i64, pub broker_out_time: i64, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub properties: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActiveMqDestination { #[serde(default)] pub physical_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] mod test { use super::*; + use crate::fixtures::verify_serde_roundtrip; #[test] #[cfg(feature = "activemq")] fn example_activemq_event() { - let data = include_bytes!("../../fixtures/example-activemq-event.json"); - let parsed: ActiveMqEvent = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: ActiveMqEvent = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(include_bytes!("../../fixtures/example-activemq-event.json")); + } + + #[test] + #[cfg(feature = "activemq")] + fn example_activemq_event_null_properties() { + let event: ActiveMqEvent = verify_serde_roundtrip(include_bytes!( + "../../fixtures/example-activemq-event-null-properties.json" + )); + assert_eq!( + 0, + event.messages[0].properties.len(), + "null properties should deserialize to empty map" + ) } } diff --git a/lambda-events/src/event/alb/mod.rs b/lambda-events/src/event/alb/mod.rs index 3b4ce9d5e..429f4deb9 100644 --- a/lambda-events/src/event/alb/mod.rs +++ b/lambda-events/src/event/alb/mod.rs @@ -1,14 +1,21 @@ use crate::{ custom_serde::{ - deserialize_headers, deserialize_nullish_boolean, http_method, serialize_headers, serialize_multi_value_headers, + deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers, + serialize_query_string_parameters, }, encodings::Body, }; +#[cfg(feature = "builders")] +use bon::Builder; use http::{HeaderMap, Method}; use query_map::QueryMap; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `AlbTargetGroupRequest` contains data originating from the ALB Lambda target group integration +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlbTargetGroupRequest { @@ -17,6 +24,7 @@ pub struct AlbTargetGroupRequest { #[serde(default)] pub path: Option, #[serde(default)] + #[serde(serialize_with = "serialize_query_string_parameters")] pub query_string_parameters: QueryMap, #[serde(default)] pub multi_value_query_string_parameters: QueryMap, @@ -27,28 +35,58 @@ pub struct AlbTargetGroupRequest { #[serde(serialize_with = "serialize_multi_value_headers")] pub multi_value_headers: HeaderMap, pub request_context: AlbTargetGroupRequestContext, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, pub body: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AlbTargetGroupRequestContext` contains the information to identify the load balancer invoking the lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlbTargetGroupRequestContext { pub elb: ElbContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ElbContext` contains the information to identify the ARN invoking the lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ElbContext { /// nolint: stylecheck #[serde(default)] pub target_group_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AlbTargetGroupResponse` configures the response to be returned by the ALB Lambda target group for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlbTargetGroupResponse { @@ -63,13 +101,22 @@ pub struct AlbTargetGroupResponse { pub multi_value_headers: HeaderMap, #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] mod test { use super::*; + use serde_json::Value; #[test] #[cfg(feature = "alb")] @@ -91,6 +138,19 @@ mod test { assert_eq!(parsed, reparsed); } + #[test] + #[cfg(feature = "alb")] + fn ensure_alb_lambda_target_request_query_string_parameter_value_is_string() { + let data = include_bytes!("../../fixtures/example-alb-lambda-target-request-headers-only.json"); + let parsed: AlbTargetGroupRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: Value = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!( + reparsed["queryStringParameters"]["key"], + Value::String("hello".to_string()) + ); + } + #[test] #[cfg(feature = "alb")] fn example_alb_lambda_target_response() { diff --git a/lambda-events/src/event/apigw/mod.rs b/lambda-events/src/event/apigw/mod.rs index e0aa1e8c1..fe7e4ac6e 100644 --- a/lambda-events/src/event/apigw/mod.rs +++ b/lambda-events/src/event/apigw/mod.rs @@ -1,11 +1,12 @@ use crate::{ custom_serde::{ - deserialize_headers, deserialize_lambda_map, deserialize_nullish_boolean, http_method, serialize_headers, - serialize_multi_value_headers, + deserialize_headers, deserialize_nullish, http_method, serialize_headers, serialize_multi_value_headers, }, encodings::Body, iam::IamPolicyStatement, }; +#[cfg(feature = "builders")] +use bon::Builder; use http::{HeaderMap, Method}; use query_map::QueryMap; use serde::{de::DeserializeOwned, ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -13,6 +14,8 @@ use serde_json::Value; use std::collections::HashMap; /// `ApiGatewayProxyRequest` contains data coming from the API Gateway proxy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayProxyRequest { @@ -35,21 +38,31 @@ pub struct ApiGatewayProxyRequest { pub query_string_parameters: QueryMap, #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] pub multi_value_query_string_parameters: QueryMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub path_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, #[serde(bound = "")] pub request_context: ApiGatewayProxyRequestContext, #[serde(default)] pub body: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayProxyResponse` configures the response to be returned by API Gateway for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayProxyResponse { @@ -62,12 +75,22 @@ pub struct ApiGatewayProxyResponse { pub multi_value_headers: HeaderMap, #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayProxyRequestContext` contains the information to identify the AWS account and resources invoking the /// Lambda function. It also includes Cognito identity information for the caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayProxyRequestContext { @@ -110,20 +133,86 @@ pub struct ApiGatewayProxyRequestContext { #[serde(default)] #[serde(rename = "apiId")] pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequest` contains data coming from the new HTTP API Gateway +/// +/// # Important Note +/// +/// For standard HTTP API v2 requests, the HTTP method is located in `request_context.http.method`, +/// not in the top-level `http_method` field. The top-level `http_method` field (and other +/// authorizer-specific fields) are only present in Custom Authorizer v1 payloads. +/// +/// **For standard HTTP API v2 requests, use `request_context.http.method` to get the correct HTTP method.** +/// +/// For Custom Authorizer v1 payloads, use the dedicated `ApiGatewayV2CustomAuthorizerV1Request` struct instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequest { + /// The type field from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] #[serde(default, rename = "type")] pub kind: Option, + /// The method ARN from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] #[serde(default)] pub method_arn: Option, + /// The HTTP method field. + /// + /// **Warning**: This field defaults to `GET` if not present in the payload, which may lead to + /// incorrect behavior. For standard HTTP API v2 requests, this field is not present in the JSON payload. + /// + /// **Use `request_context.http.method` instead for standard HTTP API v2 requests**, which contains + /// the actual HTTP method for the request. + /// + /// This top-level `http_method` field is only present in Custom Authorizer v1 requests. + /// For those payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "For standard HTTP API v2 requests, use `request_context.http.method` instead. This field defaults to GET when missing and may return incorrect values. For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request`." + )] #[serde(with = "http_method", default = "default_http_method")] pub http_method: Method, + /// The identity source from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] #[serde(default)] pub identity_source: Option, + /// The authorization token from Custom Authorizer v1 requests. + /// + /// This field is not present in standard HTTP API v2 requests. + /// For Custom Authorizer v1 payloads, use `ApiGatewayV2CustomAuthorizerV1Request` instead. + #[deprecated( + since = "1.1.0", + note = "This field is only present in Custom Authorizer v1 requests. Use `ApiGatewayV2CustomAuthorizerV1Request` for authorizer payloads." + )] #[serde(default)] pub authorization_token: Option, #[serde(default)] @@ -148,21 +237,31 @@ pub struct ApiGatewayV2httpRequest { #[serde(skip_serializing_if = "QueryMap::is_empty")] #[serde(serialize_with = "query_map::serde::aws_api_gateway_v2::serialize_query_string_parameters")] pub query_string_parameters: QueryMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] pub path_parameters: HashMap, pub request_context: ApiGatewayV2httpRequestContext, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, #[serde(default)] pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequestContext` contains the information to identify the AWS account and resources invoking the Lambda function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequestContext { @@ -192,9 +291,19 @@ pub struct ApiGatewayV2httpRequestContext { pub http: ApiGatewayV2httpRequestContextHttpDescription, #[serde(skip_serializing_if = "Option::is_none")] pub authentication: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayRequestAuthorizer` contains authorizer information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct ApiGatewayRequestAuthorizer { #[serde(skip_serializing_if = "Option::is_none")] @@ -204,25 +313,45 @@ pub struct ApiGatewayRequestAuthorizer { rename = "lambda", default, skip_serializing_if = "HashMap::is_empty", - deserialize_with = "deserialize_lambda_map" + deserialize_with = "deserialize_nullish" )] pub fields: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub iam: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayRequestAuthorizerJwtDescription` contains JWT authorizer information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayRequestAuthorizerJwtDescription { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub claims: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub scopes: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayRequestAuthorizerIamDescription` contains IAM information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayRequestAuthorizerIamDescription { @@ -240,9 +369,19 @@ pub struct ApiGatewayRequestAuthorizerIamDescription { pub user_arn: Option, #[serde(default)] pub user_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayRequestAuthorizerCognitoIdentity` contains Cognito identity information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayRequestAuthorizerCognitoIdentity { @@ -251,9 +390,19 @@ pub struct ApiGatewayRequestAuthorizerCognitoIdentity { pub identity_id: Option, #[serde(default)] pub identity_pool_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequestContextHttpDescription` contains HTTP information for the request context. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequestContextHttpDescription { @@ -267,9 +416,19 @@ pub struct ApiGatewayV2httpRequestContextHttpDescription { pub source_ip: Option, #[serde(default)] pub user_agent: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpResponse` configures the response to be returned by API Gateway V2 for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpResponse { @@ -282,12 +441,22 @@ pub struct ApiGatewayV2httpResponse { pub multi_value_headers: HeaderMap, #[serde(skip_serializing_if = "Option::is_none")] pub body: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, pub cookies: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayRequestIdentity` contains identity information for the request caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayRequestIdentity { @@ -318,9 +487,19 @@ pub struct ApiGatewayRequestIdentity { pub user_agent: Option, #[serde(default)] pub user: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayWebsocketProxyRequest` contains data coming from the API Gateway proxy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayWebsocketProxyRequest { @@ -345,23 +524,33 @@ pub struct ApiGatewayWebsocketProxyRequest { pub query_string_parameters: QueryMap, #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] pub multi_value_query_string_parameters: QueryMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub path_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, #[serde(bound = "")] pub request_context: ApiGatewayWebsocketProxyRequestContext, #[serde(default)] pub body: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayWebsocketProxyRequestContext` contains the information to identify /// the AWS account and resources invoking the Lambda function. It also includes /// Cognito identity information for the caller. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayWebsocketProxyRequestContext { @@ -421,9 +610,19 @@ pub struct ApiGatewayWebsocketProxyRequestContext { pub disconnect_status_code: Option, #[serde(default)] pub disconnect_reason: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentity` contains identity information for the request caller including certificate information if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentity { @@ -435,9 +634,19 @@ pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentity { pub source_ip: Option, #[serde(default)] pub client_cert: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCert` contains certificate information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCert { @@ -452,9 +661,19 @@ pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCert { #[serde(rename = "subjectDN")] pub subject_dn: Option, pub validity: ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity` contains certificate validity information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidity { @@ -462,17 +681,37 @@ pub struct ApiGatewayCustomAuthorizerRequestTypeRequestIdentityClientCertValidit pub not_after: Option, #[serde(default)] pub not_before: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequestContextAuthentication` contains authentication context information for the request caller including client certificate information if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequestContextAuthentication { #[serde(default)] pub client_cert: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequestContextAuthenticationClientCert` contains client certificate information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequestContextAuthenticationClientCert { @@ -487,9 +726,19 @@ pub struct ApiGatewayV2httpRequestContextAuthenticationClientCert { #[serde(rename = "subjectDN")] pub subject_dn: Option, pub validity: ApiGatewayV2httpRequestContextAuthenticationClientCertValidity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2httpRequestContextAuthenticationClientCertValidity` contains client certificate validity information for the request caller if using mTLS. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2httpRequestContextAuthenticationClientCertValidity { @@ -497,8 +746,18 @@ pub struct ApiGatewayV2httpRequestContextAuthenticationClientCertValidity { pub not_after: Option, #[serde(default)] pub not_before: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2CustomAuthorizerV1RequestTypeRequestContext { @@ -520,8 +779,18 @@ pub struct ApiGatewayV2CustomAuthorizerV1RequestTypeRequestContext { #[serde(default)] #[serde(rename = "apiId")] pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2CustomAuthorizerV1Request { @@ -545,18 +814,28 @@ pub struct ApiGatewayV2CustomAuthorizerV1Request { #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] #[serde(serialize_with = "serialize_headers")] pub headers: HeaderMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub query_string_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub path_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, pub request_context: ApiGatewayV2CustomAuthorizerV1RequestTypeRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2CustomAuthorizerV2Request { @@ -580,31 +859,51 @@ pub struct ApiGatewayV2CustomAuthorizerV2Request { #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] #[serde(serialize_with = "serialize_headers")] pub headers: HeaderMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub query_string_parameters: HashMap, pub request_context: ApiGatewayV2httpRequestContext, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub path_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerContext` represents the expected format of an API Gateway custom authorizer response. /// Deprecated. Code should be updated to use the Authorizer map from APIGatewayRequestIdentity. Ex: Authorizer["principalId"] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerContext { pub principal_id: Option, pub string_key: Option, pub num_key: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub bool_key: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequestTypeRequestContext` represents the expected format of an API Gateway custom authorizer response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequestTypeRequestContext { @@ -630,9 +929,19 @@ pub struct ApiGatewayCustomAuthorizerRequestTypeRequestContext { #[serde(default)] #[serde(rename = "apiId")] pub apiid: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequest` contains data coming in to a custom API Gateway authorizer function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequest { @@ -643,9 +952,19 @@ pub struct ApiGatewayCustomAuthorizerRequest { /// nolint: stylecheck #[serde(default)] pub method_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerRequestTypeRequest` contains data coming in to a custom API Gateway authorizer function. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerRequestTypeRequest { @@ -673,16 +992,26 @@ pub struct ApiGatewayCustomAuthorizerRequestTypeRequest { pub query_string_parameters: QueryMap, #[serde(default, deserialize_with = "query_map::serde::standard::deserialize_empty")] pub multi_value_query_string_parameters: QueryMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub path_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub stage_variables: HashMap, pub request_context: ApiGatewayCustomAuthorizerRequestTypeRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerResponse` represents the expected format of an API Gateway authorization response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayCustomAuthorizerResponse @@ -696,9 +1025,19 @@ where #[serde(bound = "", default)] pub context: T1, pub usage_identifier_key: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayV2CustomAuthorizerSimpleResponse` represents the simple format of an API Gateway V2 authorization response. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2CustomAuthorizerSimpleResponse @@ -709,8 +1048,18 @@ where pub is_authorized: bool, #[serde(bound = "", default)] pub context: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApiGatewayV2CustomAuthorizerIamPolicyResponse @@ -723,15 +1072,33 @@ where pub policy_document: ApiGatewayCustomAuthorizerPolicy, #[serde(bound = "", default)] pub context: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ApiGatewayCustomAuthorizerPolicy` represents an IAM policy +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct ApiGatewayCustomAuthorizerPolicy { #[serde(default)] pub version: Option, pub statement: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } fn default_http_method() -> Method { @@ -780,6 +1147,7 @@ pub fn serialize_authorizer_fields( #[cfg(test)] mod test { use super::*; + use crate::fixtures::verify_serde_roundtrip; #[test] #[cfg(feature = "apigw")] @@ -876,6 +1244,24 @@ mod test { assert!(output.contains(r#""headername":"headerValue2""#)); } + #[test] + #[cfg(all(feature = "apigw", feature = "catch-all-fields"))] + fn example_apigw_request_catch_all() { + use serde_json::json; + + let data = include_bytes!("../../fixtures/example-apigw-request-catch-all.json"); + let parsed: ApiGatewayProxyRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ApiGatewayProxyRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + + assert_eq!(parsed.other.get("otherField"), Some(&json!("foobar"))); + assert_eq!( + parsed.request_context.identity.other.get("otherField"), + Some(&json!(2345)) + ); + } + #[test] #[cfg(feature = "apigw")] fn example_apigw_restapi_openapi_request() { @@ -939,6 +1325,22 @@ mod test { assert_eq!(parsed, reparsed); } + #[test] + #[cfg(feature = "apigw")] + fn apigw_v2_correct_http_method_usage() { + // This test demonstrates the correct way to access the HTTP method + // for standard HTTP API v2 requests: use request_context.http.method + let data = include_bytes!("../../fixtures/example-apigw-v2-request-no-authorizer.json"); + let parsed: ApiGatewayV2httpRequest = serde_json::from_slice(data).unwrap(); + + // The correct way to get the HTTP method for standard HTTP API v2 requests + assert_eq!(parsed.request_context.http.method, Method::GET); + + // Verify that request_context.http.method contains the actual method from the JSON + // In the fixture, requestContext.http.method is "GET" + assert_eq!(parsed.request_context.http.method, Method::GET); + } + #[test] #[cfg(feature = "apigw")] fn example_apigw_websocket_request() { @@ -995,20 +1397,24 @@ mod test { #[cfg(feature = "apigw")] fn example_apigw_v2_custom_authorizer_v2_request() { let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request.json"); - let parsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "apigw")] + fn example_apigw_v2_custom_authorizer_v2_request_null_maps() { + let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json"); + let parsed: ApiGatewayV2CustomAuthorizerV2Request = verify_serde_roundtrip(data); + assert_eq!(0, parsed.path_parameters.len()); + assert_eq!(0, parsed.query_string_parameters.len()); + assert_eq!(0, parsed.stage_variables.len()); } #[test] #[cfg(feature = "apigw")] fn example_apigw_v2_custom_authorizer_v2_request_without_cookies() { let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-without-cookies.json"); - let parsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); } #[test] @@ -1016,10 +1422,7 @@ mod test { fn example_apigw_v2_custom_authorizer_v2_request_without_identity_source() { let data = include_bytes!("../../fixtures/example-apigw-v2-custom-authorizer-v2-request-without-identity-source.json"); - let parsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: ApiGatewayV2CustomAuthorizerV2Request = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); } #[test] diff --git a/lambda-events/src/event/appsync/mod.rs b/lambda-events/src/event/appsync/mod.rs index 63f9ac74b..16332f5dd 100644 --- a/lambda-events/src/event/appsync/mod.rs +++ b/lambda-events/src/event/appsync/mod.rs @@ -1,11 +1,15 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; -/// Deprecated: `AppSyncResolverTemplate` does not represent resolver events sent by AppSync. Instead directly model your input schema, or use map[string]string, json.RawMessage, interface{}, etc.. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +/// Deprecated: `AppSyncResolverTemplate` does not represent resolver events sent by AppSync. Instead directly model your input schema, or use `map[string]string`, `json.RawMessage`,` interface{}`, etc.. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppSyncResolverTemplate where @@ -17,10 +21,20 @@ where pub operation: AppSyncOperation, #[serde(bound = "")] pub payload: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AppSyncIamIdentity` contains information about the caller authed via IAM. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppSyncIamIdentity { #[serde(default)] @@ -38,11 +52,22 @@ pub struct AppSyncIamIdentity { pub username: Option, #[serde(default)] pub user_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AppSyncCognitoIdentity` contains information about the caller authed via Cognito. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] +#[allow(clippy::should_implement_trait)] pub struct AppSyncCognitoIdentity where T1: DeserializeOwned, @@ -54,29 +79,49 @@ where pub issuer: Option, #[serde(default)] pub username: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub claims: HashMap, pub source_ip: Vec, #[serde(default)] pub default_auth_strategy: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type AppSyncOperation = String; /// `AppSyncLambdaAuthorizerRequest` contains an authorization request from AppSync. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppSyncLambdaAuthorizerRequest { #[serde(default)] pub authorization_token: Option, pub request_context: AppSyncLambdaAuthorizerRequestContext, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AppSyncLambdaAuthorizerRequestContext` contains the parameters of the AppSync invocation which triggered /// this authorization request. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppSyncLambdaAuthorizerRequestContext where @@ -94,14 +139,24 @@ where pub query_string: Option, #[serde(default)] pub operation_name: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub variables: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AppSyncLambdaAuthorizerResponse` represents the expected format of an authorization response to AppSync. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppSyncLambdaAuthorizerResponse where @@ -109,12 +164,194 @@ where T1: Serialize, { pub is_authorized: bool, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub resolver_context: HashMap, pub denied_fields: Option>, pub ttl_override: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncDirectResolverEvent` represents the default payload structure sent by AWS AppSync +/// when using **Direct Lambda Resolvers** (i.e., when both request and response mapping +/// templates are disabled). +/// +/// This structure includes the full AppSync **Context object**, as described in the +/// [AppSync Direct Lambda resolver reference](https://docs.aws.amazon.com/appsync/latest/devguide/direct-lambda-reference.html). +/// +/// It is recommended when working without VTL templates and relying on the standard +/// AppSync-to-Lambda event format. +/// +/// See also: +/// - [AppSync resolver mapping template context reference](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncDirectResolverEvent +where + TArguments: Serialize + DeserializeOwned, + TSource: Serialize + DeserializeOwned, + TStash: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub arguments: Option, + pub identity: Option, + #[serde(bound = "")] + pub source: Option, + pub request: AppSyncRequest, + pub info: AppSyncInfo, + #[serde(default)] + pub prev: Option, + #[serde(bound = "")] + pub stash: TStash, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncRequest` contains request-related metadata for a resolver invocation, +/// including client-sent headers and optional custom domain name. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncRequest { + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + #[serde(bound = "")] + pub headers: HashMap>, + #[serde(default)] + pub domain_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncInfo` contains metadata about the current GraphQL field being resolved. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncInfo +where + T: Serialize + DeserializeOwned, +{ + #[serde(default)] + pub selection_set_list: Vec, + #[serde(rename = "selectionSetGraphQL")] + pub selection_set_graphql: String, + pub parent_type_name: String, + pub field_name: String, + #[serde(bound = "")] + pub variables: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncPrevResult` contains the result of the previous step in a pipeline resolver. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct AppSyncPrevResult +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub result: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncIdentity` represents the identity of the caller as determined by the +/// configured AppSync authorization mechanism (IAM, Cognito, OIDC, or Lambda). +#[non_exhaustive] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(untagged, rename_all = "camelCase")] +pub enum AppSyncIdentity { + IAM(AppSyncIamIdentity), + Cognito(AppSyncCognitoIdentity), + OIDC(AppSyncIdentityOIDC), + Lambda(AppSyncIdentityLambda), +} + +impl Default for AppSyncIdentity { + fn default() -> Self { + AppSyncIdentity::IAM(AppSyncIamIdentity::default()) + } +} + +/// `AppSyncIdentityOIDC` represents identity information when using OIDC-based authorization. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[allow(clippy::should_implement_trait)] +pub struct AppSyncIdentityOIDC +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub claims: T, + pub issuer: String, + pub sub: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// `AppSyncIdentityLambda` represents identity information when using AWS Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSyncIdentityLambda +where + T: Serialize + DeserializeOwned, +{ + #[serde(bound = "")] + pub resolver_context: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] @@ -160,4 +397,14 @@ mod test { let reparsed: AppSyncLambdaAuthorizerResponse = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "appsync")] + fn example_appsync_direct_resolver() { + let data = include_bytes!("../../fixtures/example-appsync-direct-resolver.json"); + let parsed: AppSyncDirectResolverEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: AppSyncDirectResolverEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } } diff --git a/lambda-events/src/event/autoscaling/mod.rs b/lambda-events/src/event/autoscaling/mod.rs index 601e87745..d14bc1b00 100644 --- a/lambda-events/src/event/autoscaling/mod.rs +++ b/lambda-events/src/event/autoscaling/mod.rs @@ -1,12 +1,16 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; /// `AutoScalingEvent` struct is used to parse the json for auto scaling event types // -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AutoScalingEvent where @@ -37,10 +41,18 @@ where pub region: Option, /// Information about resources impacted by event pub resources: Vec, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub detail: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/bedrock_agent_runtime/mod.rs b/lambda-events/src/event/bedrock_agent_runtime/mod.rs index c1425b859..ba67b3fab 100644 --- a/lambda-events/src/event/bedrock_agent_runtime/mod.rs +++ b/lambda-events/src/event/bedrock_agent_runtime/mod.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; /// The Event sent to Lambda from Agents for Amazon Bedrock. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AgentEvent { @@ -29,22 +35,52 @@ pub struct AgentEvent { pub session_attributes: HashMap, /// Contains prompt attributes and their values. pub prompt_session_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct RequestBody { /// Contains the request body and its properties pub content: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Content { /// The content of the request body pub properties: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Property { @@ -54,8 +90,18 @@ pub struct Property { pub r#type: String, /// The value of the parameter pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Parameter { @@ -65,8 +111,18 @@ pub struct Parameter { pub r#type: String, /// The value of the parameter pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Agent { @@ -78,6 +134,14 @@ pub struct Agent { pub alias: String, /// The version of the agent. pub version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/chime_bot/mod.rs b/lambda-events/src/event/chime_bot/mod.rs index 6581ed2c8..92a6d3910 100644 --- a/lambda-events/src/event/chime_bot/mod.rs +++ b/lambda-events/src/event/chime_bot/mod.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChimeBotEvent { #[serde(rename = "Sender")] @@ -17,9 +23,19 @@ pub struct ChimeBotEvent { pub event_timestamp: DateTime, #[serde(rename = "Message")] pub message: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChimeBotEventSender { #[serde(default)] @@ -28,9 +44,19 @@ pub struct ChimeBotEventSender { #[serde(default)] #[serde(rename = "SenderIdType")] pub sender_id_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChimeBotEventDiscussion { #[serde(default)] @@ -39,9 +65,19 @@ pub struct ChimeBotEventDiscussion { #[serde(default)] #[serde(rename = "DiscussionType")] pub discussion_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChimeBotEventInboundHttpsEndpoint { #[serde(default)] @@ -50,4 +86,12 @@ pub struct ChimeBotEventInboundHttpsEndpoint { #[serde(default)] #[serde(rename = "Url")] pub url: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/clientvpn/mod.rs b/lambda-events/src/event/clientvpn/mod.rs index 163abe720..d3a6f2e42 100644 --- a/lambda-events/src/event/clientvpn/mod.rs +++ b/lambda-events/src/event/clientvpn/mod.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientVpnConnectionHandlerRequest { #[serde(default)] @@ -29,9 +35,19 @@ pub struct ClientVpnConnectionHandlerRequest { #[serde(default)] #[serde(rename = "schema-version")] pub schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClientVpnConnectionHandlerResponse { pub allow: bool, @@ -43,6 +59,14 @@ pub struct ClientVpnConnectionHandlerResponse { #[serde(default)] #[serde(rename = "schema-version")] pub schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/cloudformation/mod.rs b/lambda-events/src/event/cloudformation/mod.rs index 44156b97a..ac937ac3b 100644 --- a/lambda-events/src/event/cloudformation/mod.rs +++ b/lambda-events/src/event/cloudformation/mod.rs @@ -1,9 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; pub mod provider; +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "RequestType")] pub enum CloudFormationCustomResourceRequest @@ -19,7 +22,15 @@ where Delete(DeleteRequest), } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +impl Default for CloudFormationCustomResourceRequest { + fn default() -> Self { + CloudFormationCustomResourceRequest::Create(CreateRequest::default()) + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CreateRequest where @@ -35,9 +46,19 @@ where pub logical_resource_id: String, #[serde(bound = "")] pub resource_properties: P2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct UpdateRequest where @@ -57,9 +78,19 @@ where pub resource_properties: P2, #[serde(bound = "")] pub old_resource_properties: P1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct DeleteRequest where @@ -76,9 +107,19 @@ where pub physical_resource_id: String, #[serde(bound = "")] pub resource_properties: P2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudFormationCustomResourceResponse { pub status: CloudFormationCustomResourceResponseStatus, @@ -89,11 +130,21 @@ pub struct CloudFormationCustomResourceResponse { pub logical_resource_id: String, pub no_echo: bool, pub data: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CloudFormationCustomResourceResponseStatus { + #[default] Success, Failed, } diff --git a/lambda-events/src/event/cloudformation/provider.rs b/lambda-events/src/event/cloudformation/provider.rs index a1594eb40..4c9cbc131 100644 --- a/lambda-events/src/event/cloudformation/provider.rs +++ b/lambda-events/src/event/cloudformation/provider.rs @@ -2,11 +2,14 @@ //! //! Note that they are similar (but not the same) as the events in the `super` module. //! -//! See https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.custom_resources-readme.html for details. +//! See for details. +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "RequestType")] pub enum CloudFormationCustomResourceRequest @@ -22,7 +25,15 @@ where Delete(DeleteRequest), } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +impl Default for CloudFormationCustomResourceRequest { + fn default() -> Self { + CloudFormationCustomResourceRequest::Create(CreateRequest::default()) + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CreateRequest where @@ -30,9 +41,12 @@ where { #[serde(flatten, bound = "")] pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct UpdateRequest where @@ -46,9 +60,12 @@ where #[serde(flatten, bound = "")] pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct DeleteRequest where @@ -58,9 +75,12 @@ where #[serde(flatten, bound = "")] pub common: CommonRequestParams, + // No `other` catch-all here; any additional fields will be caught in `common.other` instead } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CommonRequestParams where @@ -72,9 +92,19 @@ where pub resource_type: String, pub request_id: String, pub stack_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Default)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudFormationCustomResourceResponse where @@ -84,6 +114,14 @@ where #[serde(bound = "")] pub data: D, pub no_echo: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/cloudwatch_alarms/mod.rs b/lambda-events/src/event/cloudwatch_alarms/mod.rs index d99f3c94d..aa66a7de1 100644 --- a/lambda-events/src/event/cloudwatch_alarms/mod.rs +++ b/lambda-events/src/event/cloudwatch_alarms/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "builders")] +use bon::Builder; use std::collections::HashMap; use chrono::{DateTime, Utc}; @@ -11,7 +13,9 @@ use serde_json::Value; /// `CloudWatchAlarm` is the generic outer structure of an event triggered by a CloudWatch Alarm. /// You probably want to use `CloudWatchMetricAlarm` or `CloudWatchCompositeAlarm` if you know which kind of alarm your function is receiving. /// For examples of events that come via CloudWatch Alarms, -/// see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#Lambda-action-payload +/// see +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarm @@ -33,6 +37,14 @@ where #[serde(default, bound = "")] pub alarm_data: CloudWatchAlarmData, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CloudWatchMetricAlarm` is the structure of an event triggered by CloudWatch metric alarms. @@ -43,6 +55,8 @@ pub type CloudWatchMetricAlarm = pub type CloudWatchCompositeAlarm = CloudWatchAlarm; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmData @@ -59,8 +73,18 @@ where pub previous_state: Option>, #[serde(bound = "")] pub configuration: C, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmState @@ -76,8 +100,18 @@ where pub timestamp: DateTime, pub actions_suppressed_by: Option, pub actions_suppressed_reason: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchMetricAlarmConfiguration { @@ -85,8 +119,18 @@ pub struct CloudWatchMetricAlarmConfiguration { pub description: Option, #[serde(default)] pub metrics: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchMetricDefinition { @@ -94,8 +138,18 @@ pub struct CloudWatchMetricDefinition { #[serde(default)] pub return_data: bool, pub metric_stat: CloudWatchMetricStatDefinition, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchMetricStatDefinition { @@ -105,8 +159,18 @@ pub struct CloudWatchMetricStatDefinition { pub stat: Option, pub period: u16, pub metric: CloudWatchMetricStatMetricDefinition, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchMetricStatMetricDefinition { @@ -114,8 +178,18 @@ pub struct CloudWatchMetricStatMetricDefinition { pub namespace: Option, pub name: String, pub dimensions: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchCompositeAlarmConfiguration { @@ -123,8 +197,17 @@ pub struct CloudWatchCompositeAlarmConfiguration { pub actions_suppressor: String, pub actions_suppressor_wait_period: u16, pub actions_suppressor_extension_period: u16, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum CloudWatchAlarmStateValue { @@ -134,6 +217,7 @@ pub enum CloudWatchAlarmStateValue { InsufficientData, } +#[non_exhaustive] #[derive(Clone, Debug, PartialEq)] pub enum CloudWatchAlarmStateReasonData { Metric(CloudWatchAlarmStateReasonDataMetric), @@ -147,6 +231,8 @@ impl Default for CloudWatchAlarmStateReasonData { } } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmStateReasonDataMetric { @@ -169,8 +255,18 @@ pub struct CloudWatchAlarmStateReasonDataMetric { pub threshold: f64, #[serde(default)] pub evaluated_datapoints: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmStateEvaluatedDatapoint { @@ -181,28 +277,66 @@ pub struct CloudWatchAlarmStateEvaluatedDatapoint { pub value: Option, #[serde(default)] pub threshold: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClodWatchAlarmStateReasonDataComposite { #[serde(default)] pub triggering_alarms: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmStateTriggeringAlarm { pub arn: String, pub state: CloudWatchAlarmStateTriggeringAlarmState, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchAlarmStateTriggeringAlarmState { pub timestamp: DateTime, #[serde(default)] pub value: CloudWatchAlarmStateValue, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } impl<'de> Deserialize<'de> for CloudWatchAlarmStateReasonData { @@ -224,7 +358,7 @@ impl Serialize for CloudWatchAlarmStateReasonData { Self::Composite(m) => serde_json::to_string(m), Self::Generic(m) => serde_json::to_string(m), }; - let s = r.map_err(|e| SerError::custom(format!("failed to serialize struct as string {}", e)))?; + let s = r.map_err(|e| SerError::custom(format!("failed to serialize struct as string {e}")))?; serializer.serialize_str(&s) } diff --git a/lambda-events/src/event/cloudwatch_events/cloudtrail.rs b/lambda-events/src/event/cloudwatch_events/cloudtrail.rs index fc3323065..2b4d8f378 100644 --- a/lambda-events/src/event/cloudwatch_events/cloudtrail.rs +++ b/lambda-events/src/event/cloudwatch_events/cloudtrail.rs @@ -1,7 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AWSAPICall { @@ -23,8 +27,18 @@ pub struct AWSAPICall { #[serde(rename = "eventID")] pub event_id: String, pub event_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SessionIssuer { @@ -33,22 +47,52 @@ pub struct SessionIssuer { pub principal_id: String, pub arn: String, pub account_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct WebIdFederationData { pub federated_provider: Option, pub attributes: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Attributes { pub mfa_authenticated: String, pub creation_date: DateTime, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SessionContext { @@ -57,16 +101,36 @@ pub struct SessionContext { pub attributes: Attributes, pub source_identity: Option, pub ec2_role_delivery: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct OnBehalfOf { pub user_id: String, pub identity_store_arn: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } // https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserIdentity { @@ -79,6 +143,14 @@ pub struct UserIdentity { pub session_context: Option, pub user_name: Option, pub on_behalf_of: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/cloudwatch_events/codedeploy.rs b/lambda-events/src/event/cloudwatch_events/codedeploy.rs index 1bd442976..7542844c1 100644 --- a/lambda-events/src/event/cloudwatch_events/codedeploy.rs +++ b/lambda-events/src/event/cloudwatch_events/codedeploy.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct StateChangeNotification { @@ -9,8 +15,18 @@ pub struct StateChangeNotification { pub deployment_id: String, pub state: String, pub deployment_group: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeploymentStateChangeNotification { @@ -21,8 +37,18 @@ pub struct DeploymentStateChangeNotification { pub deployment_id: String, pub instance_group_id: String, pub deployment_group: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct InstanceStateChangeNotification { @@ -31,4 +57,12 @@ pub struct InstanceStateChangeNotification { pub state: String, #[serde(rename = "execution-id")] pub execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/codepipeline.rs b/lambda-events/src/event/cloudwatch_events/codepipeline.rs index ce5fa47ca..a647281ea 100644 --- a/lambda-events/src/event/cloudwatch_events/codepipeline.rs +++ b/lambda-events/src/event/cloudwatch_events/codepipeline.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct PipelineExecutionStateChange { @@ -8,8 +14,18 @@ pub struct PipelineExecutionStateChange { pub state: String, #[serde(rename = "execution-id")] pub execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct StageExecutionStateChange { @@ -19,8 +35,18 @@ pub struct StageExecutionStateChange { pub execution_id: String, pub stage: String, pub state: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActionExecutionStateChange { @@ -34,8 +60,18 @@ pub struct ActionExecutionStateChange { pub region: String, #[serde(rename = "type")] pub type_field: ActionExecutionStateChangeType, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActionExecutionStateChangeType { @@ -43,4 +79,12 @@ pub struct ActionExecutionStateChangeType { pub category: String, pub provider: String, pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/ec2.rs b/lambda-events/src/event/cloudwatch_events/ec2.rs index c8eb7834f..ba668ac38 100644 --- a/lambda-events/src/event/cloudwatch_events/ec2.rs +++ b/lambda-events/src/event/cloudwatch_events/ec2.rs @@ -1,9 +1,23 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct InstanceStateChange { #[serde(rename = "instance-id")] pub instance_id: String, pub state: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/emr.rs b/lambda-events/src/event/cloudwatch_events/emr.rs index 87fb80857..d60573810 100644 --- a/lambda-events/src/event/cloudwatch_events/emr.rs +++ b/lambda-events/src/event/cloudwatch_events/emr.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AutoScalingPolicyStateChange { @@ -8,8 +14,18 @@ pub struct AutoScalingPolicyStateChange { pub state: String, pub message: String, pub scaling_resource_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClusterStateChange { @@ -19,8 +35,18 @@ pub struct ClusterStateChange { pub cluster_id: String, pub state: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct InstanceGroupStateChange { @@ -34,8 +60,18 @@ pub struct InstanceGroupStateChange { pub running_instance_count: String, pub state: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct StepStatusChange { @@ -46,4 +82,12 @@ pub struct StepStatusChange { pub cluster_id: String, pub state: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/gamelift.rs b/lambda-events/src/event/cloudwatch_events/gamelift.rs index fb5c50a75..9ff2e431d 100644 --- a/lambda-events/src/event/cloudwatch_events/gamelift.rs +++ b/lambda-events/src/event/cloudwatch_events/gamelift.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -use crate::custom_serde::deserialize_nullish_boolean; +use crate::custom_serde::deserialize_nullish; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MatchmakingSearching { @@ -9,32 +15,72 @@ pub struct MatchmakingSearching { pub estimated_wait_millis: String, pub r#type: String, pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Ticket { pub ticket_id: String, pub start_time: String, pub players: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Player { pub player_id: String, pub team: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub accepted: bool, pub player_session_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GameSessionInfo { pub players: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct PotentialMatchCreated { @@ -45,16 +91,36 @@ pub struct PotentialMatchCreated { pub r#type: String, pub game_session_info: GameSessionInfo, pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct RuleEvaluationMetric { pub rule_name: String, pub passed_count: i64, pub failed_count: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AcceptMatch { @@ -62,8 +128,18 @@ pub struct AcceptMatch { pub r#type: String, pub game_session_info: GameSessionInfo, pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AcceptMatchCompleted { @@ -72,8 +148,18 @@ pub struct AcceptMatchCompleted { pub r#type: String, pub game_session_info: GameSessionInfo, pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MatchmakingSucceeded { @@ -81,8 +167,18 @@ pub struct MatchmakingSucceeded { pub r#type: String, pub game_session_info: GameSessionInfo, pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MatchmakingTimedOut { @@ -92,8 +188,18 @@ pub struct MatchmakingTimedOut { pub r#type: String, pub message: String, pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MatchmakingCancelled { @@ -103,8 +209,18 @@ pub struct MatchmakingCancelled { pub r#type: String, pub message: String, pub game_session_info: GameSessionInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MatchmakingFailed { @@ -115,4 +231,12 @@ pub struct MatchmakingFailed { pub message: String, pub game_session_info: GameSessionInfo, pub match_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/glue.rs b/lambda-events/src/event/cloudwatch_events/glue.rs index 08f059292..d85eebd9c 100644 --- a/lambda-events/src/event/cloudwatch_events/glue.rs +++ b/lambda-events/src/event/cloudwatch_events/glue.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct JobRunStateChange { @@ -8,8 +14,18 @@ pub struct JobRunStateChange { pub state: String, pub job_run_id: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CrawlerStarted { @@ -18,8 +34,18 @@ pub struct CrawlerStarted { pub start_time: String, pub state: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CrawlerSucceeded { @@ -38,8 +64,18 @@ pub struct CrawlerSucceeded { pub state: String, pub partitions_created: String, pub cloud_watch_log_link: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CrawlerFailed { @@ -49,8 +85,18 @@ pub struct CrawlerFailed { pub cloud_watch_log_link: String, pub state: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct JobRunStatus { @@ -61,15 +107,35 @@ pub struct JobRunStatus { pub job_run_id: String, pub message: String, pub started_on: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct NotificationCondition { #[serde(rename = "NotifyDelayAfter")] pub notify_delay_after: f64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DataCatalogTableStateChange { @@ -77,12 +143,30 @@ pub struct DataCatalogTableStateChange { pub changed_partitions: Vec, pub type_of_change: String, pub table_name: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DataCatalogDatabaseStateChange { pub database_name: String, pub type_of_change: String, pub changed_tables: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/health.rs b/lambda-events/src/event/cloudwatch_events/health.rs index 2a6a82e37..727c2e546 100644 --- a/lambda-events/src/event/cloudwatch_events/health.rs +++ b/lambda-events/src/event/cloudwatch_events/health.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Event { @@ -12,18 +18,46 @@ pub struct Event { pub end_time: String, pub event_description: Vec, pub affected_entities: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EventDescription { pub language: String, pub latest_description: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Entity { pub entity_value: String, pub tags: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/kms.rs b/lambda-events/src/event/cloudwatch_events/kms.rs index 74a76e701..23e124562 100644 --- a/lambda-events/src/event/cloudwatch_events/kms.rs +++ b/lambda-events/src/event/cloudwatch_events/kms.rs @@ -1,8 +1,22 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CMKEvent { #[serde(rename = "key-id")] pub key_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/macie.rs b/lambda-events/src/event/cloudwatch_events/macie.rs index 37d18d7a0..a715070f2 100644 --- a/lambda-events/src/event/cloudwatch_events/macie.rs +++ b/lambda-events/src/event/cloudwatch_events/macie.rs @@ -1,6 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] +use serde_json::Value; use std::collections::HashMap; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Alert { @@ -18,12 +25,22 @@ pub struct Alert { pub created_at: String, pub actor: String, pub summary: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type BucketScanAlert = Alert; pub type BucketWritableAlert = Alert; pub type BucketContainsHighRiskObjectAlert = Alert; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Trigger { @@ -35,8 +52,18 @@ pub struct Trigger { pub created_at: String, pub description: String, pub risk: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BucketScanSummary { @@ -57,8 +84,18 @@ pub struct BucketScanSummary { #[serde(rename = "Events")] pub events: HashMap, pub recipient_account_id: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Ip { @@ -68,23 +105,53 @@ pub struct Ip { pub n34_205_153_2: i64, #[serde(rename = "72.21.196.70")] pub n72_21_196_70: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimeRange { pub count: i64, pub start: String, pub end: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Location { #[serde(rename = "us-east-1")] pub us_east_1: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ActionInfo { @@ -92,8 +159,18 @@ pub struct ActionInfo { #[serde(rename = "ISP")] pub isp: HashMap, pub error_code: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BucketWritableSummary { @@ -109,22 +186,52 @@ pub struct BucketWritableSummary { pub event_count: i64, #[serde(rename = "Timestamps")] pub timestamps: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Bucket { #[serde(rename = "secret-bucket-name")] pub secret_bucket_name: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Acl { #[serde(rename = "secret-bucket-name")] pub secret_bucket_name: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SecretBucketName { @@ -132,8 +239,18 @@ pub struct SecretBucketName { pub owner: Owner, #[serde(rename = "Grants")] pub grants: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Owner { @@ -141,8 +258,18 @@ pub struct Owner { pub display_name: String, #[serde(rename = "ID")] pub id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Grant { @@ -150,16 +277,36 @@ pub struct Grant { pub grantee: Grantee, #[serde(rename = "Permission")] pub permission: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Grantee { pub r#type: String, #[serde(rename = "URI")] pub uri: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BucketContainsHighRiskObjectSummary { @@ -179,8 +326,18 @@ pub struct BucketContainsHighRiskObjectSummary { pub owner: HashMap, #[serde(rename = "Timestamps")] pub timestamps: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlertUpdated { @@ -197,16 +354,36 @@ pub struct AlertUpdated { pub created_at: String, pub actor: String, pub trigger: UpdatedTrigger, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UpdatedTrigger { #[serde(rename = "alert-type")] pub alert_type: String, pub features: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct FeatureInfo { @@ -218,4 +395,12 @@ pub struct FeatureInfo { #[serde(rename = "excession_times")] pub excession_times: Vec, pub risk: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/mod.rs b/lambda-events/src/event/cloudwatch_events/mod.rs index 6384406e0..da93b2bc9 100644 --- a/lambda-events/src/event/cloudwatch_events/mod.rs +++ b/lambda-events/src/event/cloudwatch_events/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; @@ -20,8 +22,10 @@ pub mod tag; pub mod trustedadvisor; /// `CloudWatchEvent` is the outer structure of an event sent via CloudWatch Events. -/// For examples of events that come via CloudWatch Events, see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +/// For examples of events that come via CloudWatch Events, see +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CloudWatchEvent where @@ -46,4 +50,12 @@ where pub resources: Vec, #[serde(bound = "")] pub detail: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/opsworks.rs b/lambda-events/src/event/cloudwatch_events/opsworks.rs index d1c192e53..aeb210a02 100644 --- a/lambda-events/src/event/cloudwatch_events/opsworks.rs +++ b/lambda-events/src/event/cloudwatch_events/opsworks.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct InstanceStateChange { @@ -15,8 +21,18 @@ pub struct InstanceStateChange { #[serde(rename = "ec2-instance-id")] pub ec2_instance_id: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CommandStateChange { @@ -26,8 +42,18 @@ pub struct CommandStateChange { pub instance_id: String, pub r#type: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeploymentStateChange { @@ -40,8 +66,18 @@ pub struct DeploymentStateChange { pub deployment_id: String, pub command: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Alert { @@ -51,4 +87,12 @@ pub struct Alert { pub instance_id: String, pub r#type: String, pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/signin.rs b/lambda-events/src/event/cloudwatch_events/signin.rs index 1cd73e6e7..4864f5374 100644 --- a/lambda-events/src/event/cloudwatch_events/signin.rs +++ b/lambda-events/src/event/cloudwatch_events/signin.rs @@ -1,6 +1,10 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignIn { @@ -19,8 +23,18 @@ pub struct SignIn { #[serde(rename = "eventID")] pub event_id: String, pub event_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserIdentity { @@ -28,15 +42,35 @@ pub struct UserIdentity { pub principal_id: String, pub arn: String, pub account_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ResponseElements { #[serde(rename = "ConsoleLogin")] pub console_login: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct AdditionalEventData { @@ -46,4 +80,12 @@ pub struct AdditionalEventData { pub mobile_version: String, #[serde(rename = "MFAUsed")] pub mfaused: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/sms.rs b/lambda-events/src/event/cloudwatch_events/sms.rs index 7d1618229..3a63acae9 100644 --- a/lambda-events/src/event/cloudwatch_events/sms.rs +++ b/lambda-events/src/event/cloudwatch_events/sms.rs @@ -1,5 +1,11 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct JobStateChange { @@ -11,4 +17,12 @@ pub struct JobStateChange { #[serde(rename = "ami-id")] pub ami_id: Option, pub version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/ssm.rs b/lambda-events/src/event/cloudwatch_events/ssm.rs index fa6ffc3b0..d71f03e45 100644 --- a/lambda-events/src/event/cloudwatch_events/ssm.rs +++ b/lambda-events/src/event/cloudwatch_events/ssm.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2AutomationStepStatusChange { @@ -22,8 +28,18 @@ pub struct EC2AutomationStepStatusChange { pub step_name: String, #[serde(rename = "Action")] pub action: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2AutomationExecutionStatusChange { @@ -43,16 +59,36 @@ pub struct EC2AutomationExecutionStatusChange { pub time: f64, #[serde(rename = "ExecutedBy")] pub executed_by: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct StateChange { pub state: String, pub at_time: String, pub next_transition_time: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConfigurationComplianceStateChange { @@ -69,8 +105,18 @@ pub struct ConfigurationComplianceStateChange { #[serde(rename = "patch-baseline-id")] pub patch_baseline_id: Option, pub serverity: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MaintenanceWindowTargetRegistration { @@ -79,8 +125,18 @@ pub struct MaintenanceWindowTargetRegistration { #[serde(rename = "window-id")] pub window_id: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MaintenanceWindowExecutionStateChange { @@ -93,8 +149,18 @@ pub struct MaintenanceWindowExecutionStateChange { #[serde(rename = "window-execution-id")] pub window_execution_id: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MaintenanceWindowTaskExecutionStateChange { @@ -109,8 +175,18 @@ pub struct MaintenanceWindowTaskExecutionStateChange { #[serde(rename = "window-execution-id")] pub window_execution_id: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MaintenanceWindowTaskTargetInvocationStateChange { @@ -129,16 +205,36 @@ pub struct MaintenanceWindowTaskTargetInvocationStateChange { pub status: String, #[serde(rename = "owner-information")] pub owner_information: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MaintenanceWindowStateChange { #[serde(rename = "window-id")] pub window_id: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ParameterStoreStateChange { @@ -146,8 +242,18 @@ pub struct ParameterStoreStateChange { pub name: String, pub r#type: String, pub description: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2CommandStatusChange { @@ -161,8 +267,18 @@ pub struct EC2CommandStatusChange { #[serde(rename = "requested-date-time")] pub requested_date_time: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2CommandInvocationStatusChange { @@ -175,8 +291,18 @@ pub struct EC2CommandInvocationStatusChange { #[serde(rename = "requested-date-time")] pub requested_date_time: String, pub status: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2StateManagerAssociationStateChange { @@ -204,8 +330,18 @@ pub struct EC2StateManagerAssociationStateChange { pub schedule_expression: String, #[serde(rename = "association-cwe-version")] pub association_cwe_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EC2StateManagerInstanceAssociationStateChange { @@ -235,4 +371,12 @@ pub struct EC2StateManagerInstanceAssociationStateChange { pub output_url: String, #[serde(rename = "instance-association-cwe-version")] pub instance_association_cwe_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/tag.rs b/lambda-events/src/event/cloudwatch_events/tag.rs index d5bc96815..076af690b 100644 --- a/lambda-events/src/event/cloudwatch_events/tag.rs +++ b/lambda-events/src/event/cloudwatch_events/tag.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; use serde::{Deserialize, Serialize}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TagChangeOnResource { @@ -12,4 +18,12 @@ pub struct TagChangeOnResource { pub resource_type: String, pub version: i64, pub tags: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs b/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs index 6a7e25d39..f852c9230 100644 --- a/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs +++ b/lambda-events/src/event/cloudwatch_events/trustedadvisor.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CheckItemRefreshNotification { @@ -12,4 +18,12 @@ pub struct CheckItemRefreshNotification { #[serde(rename = "resource_id")] pub resource_id: String, pub uuid: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/cloudwatch_logs/mod.rs b/lambda-events/src/event/cloudwatch_logs/mod.rs index 0c9ad4a8a..70a9361aa 100644 --- a/lambda-events/src/event/cloudwatch_logs/mod.rs +++ b/lambda-events/src/event/cloudwatch_logs/mod.rs @@ -1,19 +1,37 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{ de::{Error, MapAccess, Visitor}, ser::{Error as SeError, SerializeStruct}, Deserialize, Deserializer, Serialize, Serializer, }; +#[cfg(feature = "catch-all-fields")] +#[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] +use serde_json::Value; + use std::{fmt, io::BufReader}; /// `LogsEvent` represents the raw event sent by CloudWatch +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct LogsEvent { /// `aws_logs` is gzipped and base64 encoded, it needs a custom deserializer #[serde(rename = "awslogs")] pub aws_logs: AwsLogs, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `AwsLogs` is an unmarshaled, ungzipped, CloudWatch logs event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct AwsLogs { /// `data` is the log data after is decompressed @@ -21,6 +39,8 @@ pub struct AwsLogs { } /// `LogData` represents the logs group event information +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct LogData { @@ -36,9 +56,19 @@ pub struct LogData { pub message_type: String, /// Entries in the log batch pub log_events: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LogEntry` represents a log entry from cloudwatch logs +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct LogEntry { /// Unique id for the entry @@ -47,6 +77,14 @@ pub struct LogEntry { pub timestamp: i64, /// Message published in the application log pub message: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } impl<'de> Deserialize<'de> for AwsLogs { diff --git a/lambda-events/src/event/code_commit/mod.rs b/lambda-events/src/event/code_commit/mod.rs index 7d5edfaaf..dc6574a33 100644 --- a/lambda-events/src/event/code_commit/mod.rs +++ b/lambda-events/src/event/code_commit/mod.rs @@ -1,20 +1,36 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -use crate::custom_serde::deserialize_nullish_boolean; +use crate::custom_serde::deserialize_nullish; /// `CodeCommitEvent` represents a CodeCommit event -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeCommitEvent { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type CodeCommitEventTime = DateTime; /// `CodeCommitRecord` represents a CodeCommit record -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeCommitRecord { #[serde(default)] @@ -44,25 +60,56 @@ pub struct CodeCommitRecord { pub aws_region: Option, pub event_total_parts: u64, pub custom_data: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeCommitCodeCommit` represents a CodeCommit object in a record -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeCommitCodeCommit { pub references: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeCommitReference` represents a Reference object in a CodeCommit object -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeCommitReference { #[serde(default)] pub commit: Option, #[serde(default)] pub ref_: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub created: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/codebuild/mod.rs b/lambda-events/src/event/codebuild/mod.rs index d4970f5a3..26ad4c165 100644 --- a/lambda-events/src/event/codebuild/mod.rs +++ b/lambda-events/src/event/codebuild/mod.rs @@ -2,6 +2,8 @@ use crate::{ custom_serde::{codebuild_time, CodeBuildNumber}, encodings::{MinuteDuration, SecondDuration}, }; +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; @@ -11,8 +13,10 @@ pub type CodeBuildPhaseStatus = String; pub type CodeBuildPhaseType = String; /// `CodeBuildEvent` is documented at: -/// https://docs.aws.amazon.com/codebuild/latest/userguide/sample-build-notifications.html#sample-build-notifications-ref -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildEvent { /// AccountID is the id of the AWS account from which the event originated. @@ -44,10 +48,20 @@ pub struct CodeBuildEvent { /// Detail contains information specific to a build state-change or /// build phase-change event. pub detail: CodeBuildEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildEventDetail` represents the all details related to the code build event -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildEventDetail { #[serde(rename = "build-status")] @@ -84,10 +98,20 @@ pub struct CodeBuildEventDetail { #[serde(default)] #[serde(with = "codebuild_time::optional_time")] pub completed_phase_end: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildEventAdditionalInformation` represents additional information to the code build event -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildEventAdditionalInformation { pub artifact: CodeBuildArtifact, @@ -109,10 +133,20 @@ pub struct CodeBuildEventAdditionalInformation { pub source_version: Option, pub logs: CodeBuildLogs, pub phases: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildArtifact` represents the artifact provided to build -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildArtifact { #[serde(default)] @@ -123,10 +157,20 @@ pub struct CodeBuildArtifact { pub sha256_sum: Option, #[serde(default)] pub location: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildEnvironment` represents the environment for a build -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildEnvironment { #[serde(default)] @@ -140,10 +184,20 @@ pub struct CodeBuildEnvironment { pub type_: Option, #[serde(rename = "environment-variables")] pub environment_variables: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildEnvironmentVariable` encapsulate environment variables for the code build -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildEnvironmentVariable { /// Name is the name of the environment variable. @@ -155,20 +209,40 @@ pub struct CodeBuildEnvironmentVariable { /// Value is the value of the environment variable. #[serde(default)] pub value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildSource` represent the code source will be build -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildSource { #[serde(default)] pub location: Option, #[serde(default)] pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildLogs` gives the log details of a code build -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildLogs { #[serde(default)] @@ -180,10 +254,20 @@ pub struct CodeBuildLogs { #[serde(default)] #[serde(rename = "deep-link")] pub deep_link: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodeBuildPhase` represents the phase of a build and its details -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeBuildPhase where @@ -206,6 +290,14 @@ where pub phase_type: CodeBuildPhaseType, #[serde(rename = "phase-status")] pub phase_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type CodeBuildTime = DateTime; diff --git a/lambda-events/src/event/codedeploy/mod.rs b/lambda-events/src/event/codedeploy/mod.rs index d51bf8aa6..7f3944b8d 100644 --- a/lambda-events/src/event/codedeploy/mod.rs +++ b/lambda-events/src/event/codedeploy/mod.rs @@ -1,11 +1,17 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; pub type CodeDeployDeploymentState = String; /// `CodeDeployEvent` is documented at: -/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#acd_event_types -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeDeployEvent { /// AccountID is the id of the AWS account from which the event originated. @@ -38,9 +44,19 @@ pub struct CodeDeployEvent { pub resources: Vec, /// Detail contains information specific to a deployment event. pub detail: CodeDeployEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeDeployEventDetail { /// InstanceGroupID is the ID of the instance group. @@ -63,13 +79,31 @@ pub struct CodeDeployEventDetail { /// DeploymentGroup is the name of the deployment group. #[serde(default)] pub deployment_group: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "PascalCase")] pub struct CodeDeployLifecycleEvent { pub deployment_id: String, pub lifecycle_event_hook_execution_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/codepipeline_cloudwatch/mod.rs b/lambda-events/src/event/codepipeline_cloudwatch/mod.rs index 22db26b15..5ffe7a9ca 100644 --- a/lambda-events/src/event/codepipeline_cloudwatch/mod.rs +++ b/lambda-events/src/event/codepipeline_cloudwatch/mod.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; pub type CodePipelineStageState = String; @@ -8,8 +12,10 @@ pub type CodePipelineState = String; pub type CodePipelineActionState = String; /// CodePipelineEvent is documented at: -/// https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#codepipeline_event_type -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineCloudWatchEvent { /// Version is the version of the event's schema. @@ -42,9 +48,19 @@ pub struct CodePipelineCloudWatchEvent { pub resources: Vec, /// Detail contains information specific to a deployment event. pub detail: CodePipelineEventDetail, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineEventDetail { #[serde(default)] @@ -62,9 +78,19 @@ pub struct CodePipelineEventDetail { #[serde(default)] pub region: Option, pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineEventDetailType { #[serde(default)] @@ -75,6 +101,14 @@ pub struct CodePipelineEventDetailType { pub provider: Option, /// From published EventBridge schema registry this is always int64 not string as documented pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/codepipeline_job/mod.rs b/lambda-events/src/event/codepipeline_job/mod.rs index 888e77b7f..388accca5 100644 --- a/lambda-events/src/event/codepipeline_job/mod.rs +++ b/lambda-events/src/event/codepipeline_job/mod.rs @@ -1,15 +1,31 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `CodePipelineJobEvent` contains data from an event sent from AWS CodePipeline -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineJobEvent { #[serde(rename = "CodePipeline.job")] pub code_pipeline_job: CodePipelineJob, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineJob` represents a job from an AWS CodePipeline event -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineJob { #[serde(default)] @@ -17,10 +33,20 @@ pub struct CodePipelineJob { #[serde(default)] pub account_id: Option, pub data: CodePipelineData, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineData` represents a job from an AWS CodePipeline event -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineData { pub action_configuration: CodePipelineActionConfiguration, @@ -30,17 +56,37 @@ pub struct CodePipelineData { pub artifact_credentials: CodePipelineArtifactCredentials, #[serde(default)] pub continuation_token: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineActionConfiguration` represents an Action Configuration -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineActionConfiguration { pub configuration: CodePipelineConfiguration, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineConfiguration` represents a configuration for an Action Configuration -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineConfiguration { #[serde(default)] @@ -49,60 +95,120 @@ pub struct CodePipelineConfiguration { #[serde(default)] #[serde(rename = "UserParameters")] pub user_parameters: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineInputArtifact` represents an input artifact -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineInputArtifact { pub location: CodePipelineInputLocation, pub revision: Option, #[serde(default)] pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineInputLocation` represents a input location -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineInputLocation { pub s3_location: CodePipelineS3Location, #[serde(default)] #[serde(rename = "type")] pub location_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineS3Location` represents an s3 input location -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineS3Location { #[serde(default)] pub bucket_name: Option, #[serde(default)] pub object_key: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineOutputArtifact` represents an output artifact -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineOutputArtifact { pub location: CodePipelineInputLocation, pub revision: Option, #[serde(default)] pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineOutputLocation` represents a output location -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineOutputLocation { pub s3_location: CodePipelineS3Location, #[serde(default)] #[serde(rename = "type")] pub location_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CodePipelineArtifactCredentials` represents CodePipeline artifact credentials -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodePipelineArtifactCredentials { #[serde(default)] @@ -111,6 +217,14 @@ pub struct CodePipelineArtifactCredentials { pub session_token: Option, #[serde(default)] pub access_key_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/cognito/mod.rs b/lambda-events/src/event/cognito/mod.rs index a0ebd8d9f..2826fc6cd 100644 --- a/lambda-events/src/event/cognito/mod.rs +++ b/lambda-events/src/event/cognito/mod.rs @@ -1,16 +1,20 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::{deserialize_lambda_map, deserialize_nullish_boolean}; +use crate::custom_serde::deserialize_nullish; /// `CognitoEvent` contains data from an event sent from AWS Cognito Sync +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEvent { #[serde(default)] pub dataset_name: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub dataset_records: HashMap, #[serde(default)] @@ -22,9 +26,19 @@ pub struct CognitoEvent { #[serde(default)] pub region: Option, pub version: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoDatasetRecord` represents a record from an AWS Cognito Sync event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoDatasetRecord { @@ -34,10 +48,20 @@ pub struct CognitoDatasetRecord { pub old_value: Option, #[serde(default)] pub op: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreSignup` is sent by AWS Cognito User Pools when a user attempts to register /// (sign up), allowing a Lambda to perform custom validation to accept or deny the registration request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreSignup { @@ -46,8 +70,17 @@ pub struct CognitoEventUserPoolsPreSignup { pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsPreSignupRequest, pub response: CognitoEventUserPoolsPreSignupResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsPreSignupTriggerSource { #[serde(rename = "PreSignUp_SignUp")] @@ -61,6 +94,8 @@ pub enum CognitoEventUserPoolsPreSignupTriggerSource { /// `CognitoEventUserPoolsPreAuthentication` is sent by AWS Cognito User Pools when a user submits their information /// to be authenticated, allowing you to perform custom validations to accept or deny the sign in request. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreAuthentication { @@ -70,8 +105,17 @@ pub struct CognitoEventUserPoolsPreAuthentication { CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsPreAuthenticationRequest, pub response: CognitoEventUserPoolsPreAuthenticationResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsPreAuthenticationTriggerSource { #[serde(rename = "PreAuthentication_Authentication")] @@ -81,6 +125,8 @@ pub enum CognitoEventUserPoolsPreAuthenticationTriggerSource { /// `CognitoEventUserPoolsPostConfirmation` is sent by AWS Cognito User Pools after a user is confirmed, /// allowing the Lambda to send custom messages or add custom logic. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPostConfirmation @@ -95,8 +141,17 @@ where pub request: CognitoEventUserPoolsPostConfirmationRequest, #[serde(bound = "")] pub response: T, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsPostConfirmationTriggerSource { #[serde(rename = "PostConfirmation_ConfirmForgotPassword")] @@ -108,6 +163,8 @@ pub enum CognitoEventUserPoolsPostConfirmationTriggerSource { /// `CognitoEventUserPoolsPreTokenGen` is sent by AWS Cognito User Pools when a user attempts to retrieve /// credentials, allowing a Lambda to perform insert, suppress or override claims +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGen { @@ -116,8 +173,17 @@ pub struct CognitoEventUserPoolsPreTokenGen { pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsPreTokenGenRequest, pub response: CognitoEventUserPoolsPreTokenGenResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsPreTokenGenTriggerSource { #[serde(rename = "TokenGeneration_HostedAuth")] @@ -135,6 +201,8 @@ pub enum CognitoEventUserPoolsPreTokenGenTriggerSource { /// `CognitoEventUserPoolsPostAuthentication` is sent by AWS Cognito User Pools after a user is authenticated, /// allowing the Lambda to add custom logic. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPostAuthentication { @@ -144,8 +212,17 @@ pub struct CognitoEventUserPoolsPostAuthentication { CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsPostAuthenticationRequest, pub response: CognitoEventUserPoolsPostAuthenticationResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsPostAuthenticationTriggerSource { #[serde(rename = "PostAuthentication_Authentication")] @@ -155,6 +232,8 @@ pub enum CognitoEventUserPoolsPostAuthenticationTriggerSource { /// `CognitoEventUserPoolsMigrateUser` is sent by AWS Cognito User Pools when a user does not exist in the /// user pool at the time of sign-in with a password, or in the forgot-password flow. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsMigrateUser { @@ -165,8 +244,17 @@ pub struct CognitoEventUserPoolsMigrateUser { pub cognito_event_user_pools_migrate_user_request: CognitoEventUserPoolsMigrateUserRequest, #[serde(rename = "response")] pub cognito_event_user_pools_migrate_user_response: CognitoEventUserPoolsMigrateUserResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsMigrateUserTriggerSource { #[serde(rename = "UserMigration_Authentication")] @@ -177,6 +265,8 @@ pub enum CognitoEventUserPoolsMigrateUserTriggerSource { } /// `CognitoEventUserPoolsCallerContext` contains information about the caller +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCallerContext { @@ -185,9 +275,19 @@ pub struct CognitoEventUserPoolsCallerContext { pub awssdk_version: Option, #[serde(default)] pub client_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsHeader` contains common data from events sent by AWS Cognito User Pools +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsHeader { @@ -202,85 +302,175 @@ pub struct CognitoEventUserPoolsHeader { pub caller_context: CognitoEventUserPoolsCallerContext, #[serde(default)] pub user_name: Option, + // no `other` catch-all, because this struct is itself #[serde(flatten)]-ed + // into a different struct that contains an `other` catch-all, so any + // additional fields will be caught there instead } /// `CognitoEventUserPoolsPreSignupRequest` contains the request portion of a PreSignup event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreSignupRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub validation_data: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreSignupResponse` contains the response portion of a PreSignup event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreSignupResponse { pub auto_confirm_user: bool, pub auto_verify_email: bool, pub auto_verify_phone: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreAuthenticationRequest` contains the request portion of a PreAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreAuthenticationRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub validation_data: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreAuthenticationResponse` contains the response portion of a PreAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct CognitoEventUserPoolsPreAuthenticationResponse {} +pub struct CognitoEventUserPoolsPreAuthenticationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} /// `CognitoEventUserPoolsPostConfirmationRequest` contains the request portion of a PostConfirmation event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPostConfirmationRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPostConfirmationResponse` contains the response portion of a PostConfirmation event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct CognitoEventUserPoolsPostConfirmationResponse {} +pub struct CognitoEventUserPoolsPostConfirmationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} /// `CognitoEventUserPoolsPreTokenGenRequest` contains request portion of PreTokenGen event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGenRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, + /// Group and role overrides. Note that null or missing values in the request + /// deserialize to [`GroupConfiguration::default`]. + #[serde(default, deserialize_with = "deserialize_nullish")] pub group_configuration: GroupConfiguration, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreTokenGenResponse` contains the response portion of a PreTokenGen event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGenResponse { pub claims_override_details: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreTokenGenV2` is sent by AWS Cognito User Pools when a user attempts to retrieve /// credentials, allowing a Lambda to perform insert, suppress or override claims. This is the Version 2 Payload +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGenV2 { @@ -289,90 +479,184 @@ pub struct CognitoEventUserPoolsPreTokenGenV2 { pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsPreTokenGenRequestV2, pub response: CognitoEventUserPoolsPreTokenGenResponseV2, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPreTokenGenRequestV2` contains request portion of PreTokenGenV2 event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGenRequestV2 { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, + /// Group and role overrides. Note that null or missing values in the request + /// deserialize to [`GroupConfiguration::default`]. + #[serde(default, deserialize_with = "deserialize_nullish")] pub group_configuration: GroupConfiguration, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, pub scopes: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPreTokenGenResponseV2 { pub claims_and_scope_override_details: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ClaimsAndScopeOverrideDetailsV2` allows lambda to add, suppress or override claims in the token +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClaimsAndScopeOverrideDetailsV2 { pub group_override_details: GroupConfiguration, pub id_token_generation: Option, pub access_token_generation: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoIdTokenGenerationV2` allows lambda to customize the ID Token before generation +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoIdTokenGenerationV2 { - pub claims_to_add_or_override: HashMap, + pub claims_to_add_or_override: HashMap, pub claims_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoAccessTokenGenerationV2` allows lambda to customize the Access Token before generation +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoAccessTokenGenerationV2 { - pub claims_to_add_or_override: HashMap, + pub claims_to_add_or_override: HashMap, pub claims_to_suppress: Vec, pub scopes_to_add: Vec, pub scopes_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPostAuthenticationRequest` contains the request portion of a PostAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsPostAuthenticationRequest { pub new_device_used: bool, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsPostAuthenticationResponse` contains the response portion of a PostAuthentication event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub struct CognitoEventUserPoolsPostAuthenticationResponse {} +pub struct CognitoEventUserPoolsPostAuthenticationResponse { + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} /// `CognitoEventUserPoolsMigrateUserRequest` contains the request portion of a MigrateUser event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsMigrateUserRequest { #[serde(default)] pub password: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub validation_data: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsMigrateUserResponse` contains the response portion of a MigrateUser event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsMigrateUserResponse { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, #[serde(default)] @@ -381,32 +665,62 @@ pub struct CognitoEventUserPoolsMigrateUserResponse { pub message_action: Option, #[serde(default)] pub desired_delivery_mediums: Option>, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub force_alias_creation: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ClaimsOverrideDetails` allows lambda to add, suppress or override claims in the token +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ClaimsOverrideDetails { pub group_override_details: GroupConfiguration, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub claims_to_add_or_override: HashMap, pub claims_to_suppress: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `GroupConfiguration` allows lambda to override groups, roles and set a preferred role +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GroupConfiguration { pub groups_to_override: Vec, pub iam_roles_to_override: Vec, pub preferred_role: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsChallengeResult` represents a challenge that is presented to the user in the authentication /// process that is underway, along with the corresponding result. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsChallengeResult { @@ -415,36 +729,66 @@ pub struct CognitoEventUserPoolsChallengeResult { pub challenge_result: bool, #[serde(default)] pub challenge_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsDefineAuthChallengeRequest` defines auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsDefineAuthChallengeRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, pub session: Vec>, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, #[serde(default)] pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsDefineAuthChallengeResponse` defines auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsDefineAuthChallengeResponse { #[serde(default)] pub challenge_name: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub issue_tokens: bool, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub fail_authentication: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsDefineAuthChallenge` sent by AWS Cognito User Pools to initiate custom authentication flow +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsDefineAuthChallenge { @@ -454,8 +798,17 @@ pub struct CognitoEventUserPoolsDefineAuthChallenge { CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsDefineAuthChallengeRequest, pub response: CognitoEventUserPoolsDefineAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsDefineAuthChallengeTriggerSource { #[serde(rename = "DefineAuthChallenge_Authentication")] @@ -464,37 +817,59 @@ pub enum CognitoEventUserPoolsDefineAuthChallengeTriggerSource { } /// `CognitoEventUserPoolsCreateAuthChallengeRequest` defines create auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCreateAuthChallengeRequest { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, #[serde(default)] pub challenge_name: Option, pub session: Vec>, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, #[serde(default)] pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsCreateAuthChallengeResponse` defines create auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCreateAuthChallengeResponse { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub public_challenge_parameters: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub private_challenge_parameters: HashMap, #[serde(default)] pub challenge_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsCreateAuthChallenge` sent by AWS Cognito User Pools to create a challenge to present to the user +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCreateAuthChallenge { @@ -504,8 +879,17 @@ pub struct CognitoEventUserPoolsCreateAuthChallenge { CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsCreateAuthChallengeRequest, pub response: CognitoEventUserPoolsCreateAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsCreateAuthChallengeTriggerSource { #[serde(rename = "CreateAuthChallenge_Authentication")] @@ -514,6 +898,8 @@ pub enum CognitoEventUserPoolsCreateAuthChallengeTriggerSource { } /// `CognitoEventUserPoolsVerifyAuthChallengeRequest` defines verify auth challenge request parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsVerifyAuthChallengeRequest @@ -521,31 +907,51 @@ where T1: DeserializeOwned, T1: Serialize, { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub user_attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub private_challenge_parameters: HashMap, #[serde(bound = "")] pub challenge_answer: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, #[serde(default)] pub user_not_found: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsVerifyAuthChallengeResponse` defines verify auth challenge response parameters +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsVerifyAuthChallengeResponse { - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub answer_correct: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsVerifyAuthChallenge` sent by AWS Cognito User Pools to verify if the response from the end user /// for a custom Auth Challenge is valid or not +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsVerifyAuthChallenge { @@ -555,8 +961,17 @@ pub struct CognitoEventUserPoolsVerifyAuthChallenge { CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsVerifyAuthChallengeRequest, pub response: CognitoEventUserPoolsVerifyAuthChallengeResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsVerifyAuthChallengeTriggerSource { #[serde(rename = "VerifyAuthChallengeResponse_Authentication")] @@ -566,6 +981,8 @@ pub enum CognitoEventUserPoolsVerifyAuthChallengeTriggerSource { /// `CognitoEventUserPoolsCustomMessage` is sent by AWS Cognito User Pools before a verification or MFA message is sent, /// allowing a user to customize the message dynamically. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCustomMessage { @@ -574,8 +991,17 @@ pub struct CognitoEventUserPoolsCustomMessage { pub cognito_event_user_pools_header: CognitoEventUserPoolsHeader, pub request: CognitoEventUserPoolsCustomMessageRequest, pub response: CognitoEventUserPoolsCustomMessageResponse, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] pub enum CognitoEventUserPoolsCustomMessageTriggerSource { #[serde(rename = "CustomMessage_SignUp")] @@ -596,6 +1022,8 @@ pub enum CognitoEventUserPoolsCustomMessageTriggerSource { } /// `CognitoEventUserPoolsCustomMessageRequest` contains the request portion of a CustomMessage event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCustomMessageRequest @@ -603,7 +1031,7 @@ where T1: DeserializeOwned, T1: Serialize, { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub user_attributes: HashMap, @@ -611,12 +1039,22 @@ where pub code_parameter: Option, #[serde(default)] pub username_parameter: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub client_metadata: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `CognitoEventUserPoolsCustomMessageResponse` contains the response portion of a CustomMessage event +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CognitoEventUserPoolsCustomMessageResponse { @@ -626,11 +1064,20 @@ pub struct CognitoEventUserPoolsCustomMessageResponse { pub email_message: Option, #[serde(default)] pub email_subject: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] mod test { use super::*; + use crate::fixtures::verify_serde_roundtrip; #[test] #[cfg(feature = "cognito")] @@ -720,10 +1167,27 @@ mod test { #[cfg(feature = "cognito")] fn example_cognito_event_userpools_migrateuser() { let data = include_bytes!("../../fixtures/example-cognito-event-userpools-migrateuser.json"); - let parsed: CognitoEventUserPoolsMigrateUser = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: CognitoEventUserPoolsMigrateUser = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_migrateuser_null_fields() { + let data = include_bytes!("../../fixtures/example-cognito-event-userpools-migrateuser-null-fields.json"); + let event: CognitoEventUserPoolsMigrateUser = verify_serde_roundtrip(data); + // verify correct values substituted for nulls. + assert_eq!( + 0, + event + .cognito_event_user_pools_migrate_user_response + .user_attributes + .len() + ); + assert!( + !event + .cognito_event_user_pools_migrate_user_response + .force_alias_creation + ); } #[test] @@ -770,40 +1234,52 @@ mod test { #[cfg(feature = "cognito")] fn example_cognito_event_userpools_pretokengen_incoming() { let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-incoming.json"); - let parsed: CognitoEventUserPoolsPreTokenGen = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: CognitoEventUserPoolsPreTokenGen = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); } #[test] #[cfg(feature = "cognito")] fn example_cognito_event_userpools_pretokengen_v2_incoming() { let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-v2-incoming.json"); - let parsed: CognitoEventUserPoolsPreTokenGenV2 = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: CognitoEventUserPoolsPreTokenGenV2 = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); } #[test] #[cfg(feature = "cognito")] fn example_cognito_event_userpools_pretokengen() { let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen.json"); - let parsed: CognitoEventUserPoolsPreTokenGen = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: CognitoEventUserPoolsPreTokenGen = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_pretokengen_null_group_configuration() { + let data = + include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json"); + verify_serde_roundtrip::(data); } #[test] #[cfg(feature = "cognito")] fn example_cognito_event_userpools_v2_pretokengen() { let data = include_bytes!("../../fixtures/example-cognito-event-userpools-pretokengen-v2.json"); - let parsed: CognitoEventUserPoolsPreTokenGenV2 = serde_json::from_slice(data).unwrap(); - let output: String = serde_json::to_string(&parsed).unwrap(); - let reparsed: CognitoEventUserPoolsPreTokenGenV2 = serde_json::from_slice(output.as_bytes()).unwrap(); - assert_eq!(parsed, reparsed); + verify_serde_roundtrip::(data); + } + + #[test] + #[cfg(feature = "cognito")] + fn example_cognito_event_userpools_v2_pretokengen_null_group_configuration() { + let data = include_bytes!( + "../../fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json" + ); + let event: CognitoEventUserPoolsPreTokenGenV2 = verify_serde_roundtrip(data); + // fail if there are any breaking changes to GroupConfiguration::default(). + assert_eq!(0, event.request.group_configuration.groups_to_override.len()); + assert_eq!(0, event.request.group_configuration.iam_roles_to_override.len()); + assert!(event.request.group_configuration.preferred_role.is_none()); + + #[cfg(feature = "catch-all-fields")] + assert_eq!(0, event.request.group_configuration.other.len()); } #[test] @@ -855,7 +1331,8 @@ mod test { assert!(parsed.request.user_not_found); - let output: String = serde_json::to_string(&parsed).unwrap(); + let output: String = serde_json::to_string_pretty(&parsed).unwrap(); + println!("output is: {output}"); let reparsed: CognitoEventUserPoolsVerifyAuthChallenge = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } diff --git a/lambda-events/src/event/config/mod.rs b/lambda-events/src/event/config/mod.rs index 7c06e13b2..81cfd438f 100644 --- a/lambda-events/src/event/config/mod.rs +++ b/lambda-events/src/event/config/mod.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `ConfigEvent` contains data from an event sent from AWS Config -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConfigEvent { /// The ID of the AWS account that owns the rule @@ -34,6 +40,14 @@ pub struct ConfigEvent { pub rule_parameters: Option, #[serde(default)] pub version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/connect/mod.rs b/lambda-events/src/event/connect/mod.rs index 04f26eb5b..98925ae48 100644 --- a/lambda-events/src/event/connect/mod.rs +++ b/lambda-events/src/event/connect/mod.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; /// `ConnectEvent` contains the data structure for a Connect event. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectEvent { #[serde(rename = "Details")] @@ -13,27 +19,47 @@ pub struct ConnectEvent { #[serde(default)] #[serde(rename = "Name")] pub name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ConnectDetails` holds the details of a Connect event -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectDetails { #[serde(rename = "ContactData")] pub contact_data: ConnectContactData, /// The parameters that have been set in the Connect instance at the time of the Lambda invocation. - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "Parameters")] pub parameters: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ConnectContactData` holds all of the contact information for the user that invoked the Connect event. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectContactData { /// The custom attributes from Connect that the Lambda function was invoked with. - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "Attributes")] pub attributes: HashMap, @@ -62,10 +88,20 @@ pub struct ConnectContactData { #[serde(default)] #[serde(rename = "InstanceARN")] pub instance_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ConnectEndpoint` represents routing information. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectEndpoint { #[serde(default)] @@ -74,10 +110,20 @@ pub struct ConnectEndpoint { #[serde(default)] #[serde(rename = "Type")] pub type_: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ConnectQueue` represents a queue object. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ConnectQueue { #[serde(default)] @@ -86,6 +132,14 @@ pub struct ConnectQueue { #[serde(default)] #[serde(rename = "ARN")] pub arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type ConnectResponse = HashMap; diff --git a/lambda-events/src/event/controltower/mod.rs b/lambda-events/src/event/controltower/mod.rs new file mode 100644 index 000000000..8a03c8a46 --- /dev/null +++ b/lambda-events/src/event/controltower/mod.rs @@ -0,0 +1,467 @@ +/// AWS Control Tower lifecycle event definitions. +/// +/// Control Tower lifecycle events are delivered via EventBridge as CloudTrail +/// service events with detail-type `"AWS Service Event via CloudTrail"` and +/// source `"aws.controltower"`. +/// +/// Use with the `CloudWatchEvent` or `EventBridgeEvent` wrapper: +/// ```ignore +/// use aws_lambda_events::cloudwatch_events::CloudWatchEvent; +/// use aws_lambda_events::controltower::ControlTowerLifecycleEvent; +/// +/// let event: CloudWatchEvent = serde_json::from_slice(data)?; +/// ``` +/// +/// See +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// The CloudTrail service event delivered as the EventBridge `detail` payload +/// for Control Tower lifecycle events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ControlTowerLifecycleEvent { + pub event_version: String, + pub user_identity: UserIdentity, + pub event_time: String, + pub event_source: String, + pub event_name: String, + pub aws_region: String, + #[serde(rename = "sourceIPAddress")] + pub source_ip_address: String, + pub user_agent: String, + #[serde(rename = "eventID")] + pub event_id: String, + pub read_only: bool, + pub event_type: String, + pub service_event_details: ServiceEventDetails, + #[serde(default)] + pub management_event: Option, + #[serde(default)] + pub recipient_account_id: Option, + #[serde(default)] + pub request_parameters: Option, + #[serde(default)] + pub response_elements: Option, + #[serde(default)] + pub event_category: Option, +} + +/// User identity information for Control Tower lifecycle events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserIdentity { + pub account_id: String, + #[serde(default)] + pub invoked_by: Option, +} + +/// The service event details for Control Tower lifecycle events. +/// +/// Each variant corresponds to a specific lifecycle event type. +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum ServiceEventDetails { + CreateManagedAccountStatus(ManagedAccountStatus), + UpdateManagedAccountStatus(ManagedAccountStatus), + EnableGuardrailStatus(GuardrailStatus), + DisableGuardrailStatus(GuardrailStatus), + SetupLandingZoneStatus(LandingZoneStatus), + UpdateLandingZoneStatus(LandingZoneStatus), + RegisterOrganizationalUnitStatus(OrganizationalUnitRegistrationStatus), + DeregisterOrganizationalUnitStatus(OrganizationalUnitRegistrationStatus), + PrecheckOrganizationalUnitStatus(PrecheckOrganizationalUnitStatus), + EnableBaselineStatus(BaselineStatus), + ResetEnabledBaselineStatus(BaselineStatus), + UpdateEnabledBaselineStatus(BaselineStatus), + DisableBaselineStatus(BaselineStatus), +} + +/// An organizational unit reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationalUnit { + pub organizational_unit_name: String, + pub organizational_unit_id: String, +} + +/// An account reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Account { + pub account_name: String, + pub account_id: String, +} + +/// A guardrail (control) reference. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Guardrail { + pub guardrail_id: String, + pub guardrail_behavior: String, +} + +/// Status for `CreateManagedAccount` and `UpdateManagedAccount` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedAccountStatus { + pub organizational_unit: OrganizationalUnit, + pub account: Account, + pub state: String, + pub message: String, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `EnableGuardrail` and `DisableGuardrail` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GuardrailStatus { + pub organizational_units: Vec, + pub guardrails: Vec, + pub state: String, + pub message: String, + pub request_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `SetupLandingZone` and `UpdateLandingZone` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LandingZoneStatus { + pub state: String, + pub message: String, + pub root_organizational_id: String, + pub organizational_units: Vec, + pub accounts: Vec, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status for `RegisterOrganizationalUnit` and `DeregisterOrganizationalUnit` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationalUnitRegistrationStatus { + pub state: String, + pub message: String, + pub organizational_unit: OrganizationalUnit, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// An organizational unit with precheck failure information. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckOrganizationalUnit { + pub organizational_unit_name: String, + pub organizational_unit_id: String, + #[serde(default)] + pub failed_prechecks: Vec, +} + +/// An account with precheck failure information. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckAccount { + pub account_name: String, + pub account_id: String, + #[serde(default)] + pub failed_prechecks: Vec, +} + +/// Status for `PrecheckOrganizationalUnit` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PrecheckOrganizationalUnitStatus { + pub organizational_unit: PrecheckOrganizationalUnit, + pub accounts: Vec, + pub state: String, + pub message: String, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +/// Status summary for a baseline operation. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineStatusSummary { + pub last_operation_identifier: String, + pub status: String, +} + +/// A parameter value wrapping an untyped object. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineParameterValue { + pub untyped: BaselineUntypedValue, +} + +/// An untyped baseline parameter value. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineUntypedValue { + pub object: String, +} + +/// A baseline parameter key-value pair. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineParameter { + pub key: String, + pub value: BaselineParameterValue, +} + +/// Details about an enabled baseline. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EnabledBaselineDetails { + pub arn: String, + pub parent_identifier: String, + pub target_identifier: String, + pub baseline_identifier: String, + pub baseline_version: String, + pub status_summary: BaselineStatusSummary, + #[serde(default)] + pub parameters: Vec, +} + +/// Status for `EnableBaseline`, `ResetEnabledBaseline`, +/// `UpdateEnabledBaseline`, and `DisableBaseline` events. +/// +/// The `baseline_details` field is only present in `DisableBaseline` events. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BaselineStatus { + pub enabled_baseline_details: EnabledBaselineDetails, + #[serde(default)] + pub baseline_details: Option, + pub requested_timestamp: String, + pub completed_timestamp: String, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn example_controltower_create_managed_account() { + let data = include_bytes!("../../fixtures/example-controltower-create-managed-account.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "CreateManagedAccount"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::CreateManagedAccountStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_enable_guardrail() { + let data = include_bytes!("../../fixtures/example-controltower-enable-guardrail.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "EnableGuardrail"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::EnableGuardrailStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_setup_landing_zone() { + let data = include_bytes!("../../fixtures/example-controltower-setup-landing-zone.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "SetupLandingZone"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::SetupLandingZoneStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_register_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-register-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "RegisterOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::RegisterOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_precheck_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-precheck-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "PrecheckOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::PrecheckOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_enable_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-enable-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "EnableBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::EnableBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_disable_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-disable-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DisableBaseline"); + if let ServiceEventDetails::DisableBaselineStatus(ref status) = parsed.service_event_details { + assert!(status.baseline_details.is_some()); + } else { + panic!("Expected DisableBaselineStatus"); + } + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_managed_account() { + let data = include_bytes!("../../fixtures/example-controltower-update-managed-account.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateManagedAccount"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateManagedAccountStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_disable_guardrail() { + let data = include_bytes!("../../fixtures/example-controltower-disable-guardrail.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DisableGuardrail"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::DisableGuardrailStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_landing_zone() { + let data = include_bytes!("../../fixtures/example-controltower-update-landing-zone.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateLandingZone"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateLandingZoneStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_deregister_organizational_unit() { + let data = include_bytes!("../../fixtures/example-controltower-deregister-organizational-unit.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "DeregisterOrganizationalUnit"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::DeregisterOrganizationalUnitStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_reset_enabled_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-reset-enabled-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "ResetEnabledBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::ResetEnabledBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + fn example_controltower_update_enabled_baseline() { + let data = include_bytes!("../../fixtures/example-controltower-update-enabled-baseline.json"); + let parsed: ControlTowerLifecycleEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(parsed.event_name, "UpdateEnabledBaseline"); + assert!(matches!( + parsed.service_event_details, + ServiceEventDetails::UpdateEnabledBaselineStatus(_) + )); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: ControlTowerLifecycleEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/documentdb/events/commom_types.rs b/lambda-events/src/event/documentdb/events/commom_types.rs index 5d1bdc196..16c312137 100644 --- a/lambda-events/src/event/documentdb/events/commom_types.rs +++ b/lambda-events/src/event/documentdb/events/commom_types.rs @@ -1,44 +1,105 @@ -use std::collections::HashMap; - +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashMap; pub type AnyDocument = HashMap; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DatabaseCollection { db: String, #[serde(default)] coll: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct DocumentId { #[serde(rename = "_data")] pub data: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct DocumentKeyIdOid { #[serde(rename = "$oid")] pub oid: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct DocumentKeyId { #[serde(rename = "_id")] pub id: DocumentKeyIdOid, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct InnerTimestamp { t: usize, i: usize, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct Timestamp { #[serde(rename = "$timestamp")] pub timestamp: InnerTimestamp, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/delete_event.rs b/lambda-events/src/event/documentdb/events/delete_event.rs index 7761d62f8..1bb61dc1f 100644 --- a/lambda-events/src/event/documentdb/events/delete_event.rs +++ b/lambda-events/src/event/documentdb/events/delete_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeDeleteEvent { @@ -17,4 +23,12 @@ pub struct ChangeDeleteEvent { // operation_type: String, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/drop_database_event.rs b/lambda-events/src/event/documentdb/events/drop_database_event.rs index 273c897c6..d1c12d49d 100644 --- a/lambda-events/src/event/documentdb/events/drop_database_event.rs +++ b/lambda-events/src/event/documentdb/events/drop_database_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeDropDatabaseEvent { @@ -16,4 +22,12 @@ pub struct ChangeDropDatabaseEvent { // operation_type: String, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/drop_event.rs b/lambda-events/src/event/documentdb/events/drop_event.rs index a6f929342..5b01531f7 100644 --- a/lambda-events/src/event/documentdb/events/drop_event.rs +++ b/lambda-events/src/event/documentdb/events/drop_event.rs @@ -1,6 +1,12 @@ use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeDropEvent { @@ -15,4 +21,12 @@ pub struct ChangeDropEvent { // operation_type: String, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/insert_event.rs b/lambda-events/src/event/documentdb/events/insert_event.rs index 2f4df3972..0b560232f 100644 --- a/lambda-events/src/event/documentdb/events/insert_event.rs +++ b/lambda-events/src/event/documentdb/events/insert_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeInsertEvent { @@ -17,4 +23,12 @@ pub struct ChangeInsertEvent { //operation_type: String, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/invalidate_event.rs b/lambda-events/src/event/documentdb/events/invalidate_event.rs index 47469ff98..9757c2205 100644 --- a/lambda-events/src/event/documentdb/events/invalidate_event.rs +++ b/lambda-events/src/event/documentdb/events/invalidate_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{DocumentId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeInvalidateEvent { @@ -10,4 +16,12 @@ pub struct ChangeInvalidateEvent { #[serde(default)] cluster_time: Option, // operation_type: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/rename_event.rs b/lambda-events/src/event/documentdb/events/rename_event.rs index 8bc250fb6..b3b311843 100644 --- a/lambda-events/src/event/documentdb/events/rename_event.rs +++ b/lambda-events/src/event/documentdb/events/rename_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeRenameEvent { @@ -18,4 +24,12 @@ pub struct ChangeRenameEvent { #[serde(default)] txn_number: Option, to: DatabaseCollection, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/replace_event.rs b/lambda-events/src/event/documentdb/events/replace_event.rs index c253e2726..75be2bf48 100644 --- a/lambda-events/src/event/documentdb/events/replace_event.rs +++ b/lambda-events/src/event/documentdb/events/replace_event.rs @@ -1,7 +1,13 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeReplaceEvent { @@ -17,4 +23,12 @@ pub struct ChangeReplaceEvent { // operation_type: String, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/events/update_event.rs b/lambda-events/src/event/documentdb/events/update_event.rs index 04369cf0a..1a0de1ed3 100644 --- a/lambda-events/src/event/documentdb/events/update_event.rs +++ b/lambda-events/src/event/documentdb/events/update_event.rs @@ -1,22 +1,48 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use super::commom_types::{AnyDocument, DatabaseCollection, DocumentId, DocumentKeyId, Timestamp}; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UpdateTruncate { field: String, new_size: usize, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UpdateDescription { removed_fields: Vec, truncated_arrays: Vec, updated_fields: AnyDocument, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ChangeUpdateEvent { @@ -33,4 +59,12 @@ pub struct ChangeUpdateEvent { update_description: UpdateDescription, #[serde(default)] txn_number: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/documentdb/mod.rs b/lambda-events/src/event/documentdb/mod.rs index 67f7c9ad6..a960acf72 100644 --- a/lambda-events/src/event/documentdb/mod.rs +++ b/lambda-events/src/event/documentdb/mod.rs @@ -5,8 +5,13 @@ use self::events::{ insert_event::ChangeInsertEvent, invalidate_event::ChangeInvalidateEvent, rename_event::ChangeRenameEvent, replace_event::ChangeReplaceEvent, update_event::ChangeUpdateEvent, }; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(tag = "operationType", rename_all = "camelCase")] pub enum ChangeEvent { @@ -20,11 +25,23 @@ pub enum ChangeEvent { Rename(ChangeRenameEvent), } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct DocumentDbInnerEvent { pub event: ChangeEvent, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DocumentDbEvent { @@ -33,6 +50,14 @@ pub struct DocumentDbEvent { pub events: Vec, #[serde(default)] pub event_source: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/dynamodb/attributes.rs b/lambda-events/src/event/dynamodb/attributes.rs index e1a42c836..ac43adea4 100644 --- a/lambda-events/src/event/dynamodb/attributes.rs +++ b/lambda-events/src/event/dynamodb/attributes.rs @@ -15,7 +15,7 @@ mod test { let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); match attr { AttributeValue::Null(true) => {} - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -31,7 +31,7 @@ mod test { let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); match attr { AttributeValue::S(ref s) => assert_eq!("value", s.as_str()), - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -47,7 +47,7 @@ mod test { let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); match attr { AttributeValue::N(ref n) => assert_eq!("123.45", n.as_str()), - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -68,7 +68,7 @@ mod test { .unwrap(); assert_eq!(&expected, b) } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -84,7 +84,7 @@ mod test { let attr: AttributeValue = serde_json::from_value(value.clone()).unwrap(); match attr { AttributeValue::Bool(b) => assert!(b), - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -103,7 +103,7 @@ mod test { let expected = vec!["Giraffe", "Hippo", "Zebra"]; assert_eq!(expected, s.iter().collect::>()); } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -122,7 +122,7 @@ mod test { let expected = vec!["42.2", "-19", "7.5", "3.14"]; assert_eq!(expected, s.iter().collect::>()); } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -144,7 +144,7 @@ mod test { .collect::>(); assert_eq!(&expected, s); } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -167,7 +167,7 @@ mod test { ]; assert_eq!(&expected, s); } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } let reparsed = serde_json::to_value(attr).unwrap(); @@ -188,7 +188,7 @@ mod test { expected.insert("Age".into(), AttributeValue::N("35".into())); assert_eq!(expected, s); } - other => panic!("unexpected value {:?}", other), + other => panic!("unexpected value {other:?}"), } } } diff --git a/lambda-events/src/event/dynamodb/mod.rs b/lambda-events/src/event/dynamodb/mod.rs index 2a3d75582..0f67dc0af 100644 --- a/lambda-events/src/event/dynamodb/mod.rs +++ b/lambda-events/src/event/dynamodb/mod.rs @@ -1,21 +1,27 @@ use crate::{ - custom_serde::{deserialize_lambda_dynamodb_item, float_unix_epoch}, + custom_serde::{deserialize_nullish, float_unix_epoch}, streams::DynamoDbBatchItemFailure, time_window::*, }; +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::fmt; #[cfg(test)] mod attributes; -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum StreamViewType { NewImage, OldImage, NewAndOldImages, + #[default] KeysOnly, } @@ -27,16 +33,18 @@ impl fmt::Display for StreamViewType { StreamViewType::NewAndOldImages => "NEW_AND_OLD_IMAGES", StreamViewType::KeysOnly => "KEYS_ONLY", }; - write!(f, "{}", val) + write!(f, "{val}") } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum StreamStatus { Enabling, Enabled, Disabling, + #[default] Disabled, } @@ -48,14 +56,16 @@ impl fmt::Display for StreamStatus { StreamStatus::Disabling => "DISABLING", StreamStatus::Disabled => "DISABLED", }; - write!(f, "{}", val) + write!(f, "{val}") } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum SharedIteratorType { TrimHorizon, + #[default] Latest, AtSequenceNumber, AfterSequenceNumber, @@ -69,13 +79,15 @@ impl fmt::Display for SharedIteratorType { SharedIteratorType::AtSequenceNumber => "AT_SEQUENCE_NUMBER", SharedIteratorType::AfterSequenceNumber => "AFTER_SEQUENCE_NUMBER", }; - write!(f, "{}", val) + write!(f, "{val}") } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum OperationType { + #[default] Insert, Modify, Remove, @@ -88,13 +100,15 @@ impl fmt::Display for OperationType { OperationType::Modify => "MODIFY", OperationType::Remove => "REMOVE", }; - write!(f, "{}", val) + write!(f, "{val}") } } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum KeyType { + #[default] Hash, Range, } @@ -105,21 +119,33 @@ impl fmt::Display for KeyType { KeyType::Hash => "HASH", KeyType::Range => "RANGE", }; - write!(f, "{}", val) + write!(f, "{val}") } } /// The `Event` stream event handled to Lambda -/// http://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-ddb-update -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +/// +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Event { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `TimeWindowEvent` represents an Amazon Dynamodb event when using time windows -/// ref. https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-windows -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +/// ref. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimeWindowEvent { #[serde(rename = "DynamoDBEvent")] @@ -128,9 +154,19 @@ pub struct TimeWindowEvent { #[serde(rename = "TimeWindowProperties")] #[serde(flatten)] pub time_window_properties: TimeWindowProperties, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `TimeWindowEventResponse` is the outer structure to report batch item failures for DynamoDBTimeWindowEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimeWindowEventResponse { @@ -138,10 +174,20 @@ pub struct TimeWindowEventResponse { #[serde(flatten)] pub time_window_event_response_properties: TimeWindowEventResponseProperties, pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// EventRecord stores information about each record of a DynamoDb stream event -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EventRecord { /// The region in which the GetRecords request was received. @@ -198,8 +244,18 @@ pub struct EventRecord { /// The DynamoDB table that this event was recorded for. #[serde(default)] pub table_name: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserIdentity { @@ -207,32 +263,42 @@ pub struct UserIdentity { pub type_: String, #[serde(default)] pub principal_id: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `DynamoDbStreamRecord` represents a description of a single data modification that was performed on an item /// in a DynamoDB table. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct StreamRecord { /// The approximate date and time when the stream record was created, in UNIX - /// epoch time (http://www.epochconverter.com/) format. Might not be present in - /// the record: https://github.com/awslabs/aws-lambda-rust-runtime/issues/889 + /// epoch time () format. Might not be present in + /// the record: #[serde(rename = "ApproximateCreationDateTime")] #[serde(with = "float_unix_epoch")] #[serde(default)] pub approximate_creation_date_time: DateTime, /// The primary key attribute(s) for the DynamoDB item that was modified. - #[serde(deserialize_with = "deserialize_lambda_dynamodb_item")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "Keys")] pub keys: serde_dynamo::Item, /// The item in the DynamoDB table as it appeared after it was modified. - #[serde(deserialize_with = "deserialize_lambda_dynamodb_item")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "NewImage")] pub new_image: serde_dynamo::Item, /// The item in the DynamoDB table as it appeared before it was modified. - #[serde(deserialize_with = "deserialize_lambda_dynamodb_item")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "OldImage")] pub old_image: serde_dynamo::Item, @@ -248,12 +314,21 @@ pub struct StreamRecord { #[serde(default)] #[serde(rename = "StreamViewType")] pub stream_view_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] #[allow(deprecated)] mod test { use super::*; + use crate::fixtures::verify_serde_roundtrip; use chrono::TimeZone; #[test] @@ -270,6 +345,16 @@ mod test { assert_eq!(date, event.change.approximate_creation_date_time); } + #[test] + #[cfg(feature = "dynamodb")] + fn example_dynamodb_event_null_items() { + let data = include_bytes!("../../fixtures/example-dynamodb-event-null-items.json"); + let event = verify_serde_roundtrip::(data); + assert_eq!(0, event.records[0].change.keys.len()); + assert_eq!(0, event.records[0].change.new_image.len()); + assert_eq!(0, event.records[0].change.old_image.len()); + } + #[test] #[cfg(feature = "dynamodb")] fn example_dynamodb_event_with_optional_fields() { diff --git a/lambda-events/src/event/ecr_scan/mod.rs b/lambda-events/src/event/ecr_scan/mod.rs index 5502e81ac..588edc7e4 100644 --- a/lambda-events/src/event/ecr_scan/mod.rs +++ b/lambda-events/src/event/ecr_scan/mod.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EcrScanEvent { #[serde(default)] @@ -20,9 +26,19 @@ pub struct EcrScanEvent { #[serde(default)] pub account: Option, pub detail: EcrScanEventDetailType, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EcrScanEventDetailType { #[serde(default)] @@ -38,9 +54,19 @@ pub struct EcrScanEventDetailType { pub image_digest: Option, #[serde(rename = "image-tags")] pub image_tags: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EcrScanEventFindingSeverityCounts { #[serde(default)] @@ -61,6 +87,14 @@ pub struct EcrScanEventFindingSeverityCounts { #[serde(default)] #[serde(rename = "UNDEFINED")] pub undefined: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/eventbridge/mod.rs b/lambda-events/src/event/eventbridge/mod.rs index 7756e0e41..36dc43b09 100644 --- a/lambda-events/src/event/eventbridge/mod.rs +++ b/lambda-events/src/event/eventbridge/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; @@ -5,7 +7,9 @@ use serde_json::Value; /// Parse EventBridge events. /// Deserialize the event detail into a structure that's `DeserializeOwned`. /// -/// See https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events-structure.html for structure details. +/// See for structure details. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(bound(deserialize = "T1: DeserializeOwned"))] #[serde(rename_all = "kebab-case")] @@ -30,6 +34,14 @@ where pub resources: Option>, #[serde(bound = "")] pub detail: T1, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/firehose/mod.rs b/lambda-events/src/event/firehose/mod.rs index 6a0a13fd0..527c5ba11 100644 --- a/lambda-events/src/event/firehose/mod.rs +++ b/lambda-events/src/event/firehose/mod.rs @@ -1,12 +1,18 @@ use crate::{ - custom_serde::deserialize_lambda_map, + custom_serde::deserialize_nullish, encodings::{Base64Data, MillisecondTimestamp}, }; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; /// `KinesisFirehoseEvent` represents the input event from Amazon Kinesis Firehose. It is used as the input parameter. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseEvent { #[serde(default)] @@ -20,9 +26,19 @@ pub struct KinesisFirehoseEvent { #[serde(default)] pub region: Option, pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseEventRecord { #[serde(default)] @@ -31,15 +47,35 @@ pub struct KinesisFirehoseEventRecord { pub data: Base64Data, #[serde(rename = "kinesisRecordMetadata")] pub kinesis_firehose_record_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseResponse { pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseResponseRecord { #[serde(default)] @@ -49,17 +85,37 @@ pub struct KinesisFirehoseResponseRecord { pub result: Option, pub data: Base64Data, pub metadata: KinesisFirehoseResponseRecordMetadata, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseResponseRecordMetadata { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub partition_keys: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisFirehoseRecordMetadata { #[serde(default)] @@ -70,6 +126,14 @@ pub struct KinesisFirehoseRecordMetadata { pub sequence_number: Option, pub subsequence_number: i64, pub approximate_arrival_timestamp: MillisecondTimestamp, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/iam/mod.rs b/lambda-events/src/event/iam/mod.rs index 36f59c7b1..9a04a8dae 100644 --- a/lambda-events/src/event/iam/mod.rs +++ b/lambda-events/src/event/iam/mod.rs @@ -1,3 +1,7 @@ +#[cfg(feature = "builders")] +use bon::Builder; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::{borrow::Cow, collections::HashMap, fmt}; use serde::{ @@ -6,15 +10,27 @@ use serde::{ }; /// `IamPolicyDocument` represents an IAM policy document. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct IamPolicyDocument { #[serde(default)] pub version: Option, pub statement: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `IamPolicyStatement` represents one statement from IAM policy with action, effect and resource +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct IamPolicyStatement { @@ -27,10 +43,19 @@ pub struct IamPolicyStatement { #[serde(default, deserialize_with = "deserialize_policy_condition")] #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type IamPolicyCondition = HashMap>>; +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub enum IamPolicyEffect { #[default] @@ -178,6 +203,8 @@ mod tests { effect: IamPolicyEffect::Allow, resource: vec!["some:resource".into()], condition: None, + #[cfg(feature = "catch-all-fields")] + other: Default::default(), }; let policy_ser = serde_json::to_value(policy).unwrap(); diff --git a/lambda-events/src/event/iot/mod.rs b/lambda-events/src/event/iot/mod.rs index 3835b515b..5fe14f565 100644 --- a/lambda-events/src/event/iot/mod.rs +++ b/lambda-events/src/event/iot/mod.rs @@ -1,10 +1,16 @@ use crate::{custom_serde::serialize_headers, encodings::Base64Data, iam::IamPolicyDocument}; +#[cfg(feature = "builders")] +use bon::Builder; use http::HeaderMap; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `IoTCoreCustomAuthorizerRequest` represents the request to an IoT Core custom authorizer. -/// See https://docs.aws.amazon.com/iot/latest/developerguide/config-custom-auth.html -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +/// See +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreCustomAuthorizerRequest { #[serde(default)] @@ -13,24 +19,58 @@ pub struct IoTCoreCustomAuthorizerRequest { pub protocols: Vec, pub protocol_data: Option, pub connection_metadata: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreProtocolData { pub tls: Option, pub http: Option, pub mqtt: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreTlsContext { #[serde(default)] pub server_name: Option, + #[serde(default)] + pub x509_certificate_pem: Option, + #[serde(default)] + pub principal_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreHttpContext { #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] @@ -38,28 +78,62 @@ pub struct IoTCoreHttpContext { pub headers: HeaderMap, #[serde(default)] pub query_string: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreMqttContext { #[serde(default)] pub client_id: Option, + /// X.509 custom authorizer requests don't include a password field. + /// Default to empty `Vec` when absent. + /// Serializing result will be `password: ""` + #[serde(default)] pub password: Base64Data, #[serde(default)] pub username: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreConnectionMetadata { #[serde(default)] pub id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `IoTCoreCustomAuthorizerResponse` represents the response from an IoT Core custom authorizer. -/// See https://docs.aws.amazon.com/iot/latest/developerguide/config-custom-auth.html -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +/// See +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCoreCustomAuthorizerResponse { pub is_authenticated: bool, @@ -68,6 +142,14 @@ pub struct IoTCoreCustomAuthorizerResponse { pub disconnect_after_in_seconds: u32, pub refresh_after_in_seconds: u32, pub policy_documents: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] @@ -84,6 +166,16 @@ mod test { assert_eq!(parsed, reparsed); } + #[test] + #[cfg(feature = "iot")] + fn example_iot_custom_auth_request_x509() { + let data = include_bytes!("../../fixtures/example-iot-custom-auth-request-x509.json"); + let parsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: IoTCoreCustomAuthorizerRequest = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + #[test] #[cfg(feature = "iot")] fn example_iot_custom_auth_response() { diff --git a/lambda-events/src/event/iot_1_click/mod.rs b/lambda-events/src/event/iot_1_click/mod.rs index bf010b507..58b6e2562 100644 --- a/lambda-events/src/event/iot_1_click/mod.rs +++ b/lambda-events/src/event/iot_1_click/mod.rs @@ -1,37 +1,73 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; /// `IoTOneClickEvent` represents a click event published by clicking button type /// device. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTOneClickEvent { pub device_event: IoTOneClickDeviceEvent, pub device_info: IoTOneClickDeviceInfo, pub placement_info: IoTOneClickPlacementInfo, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTOneClickDeviceEvent { pub button_clicked: IoTOneClickButtonClicked, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTOneClickButtonClicked { #[serde(default)] pub click_type: Option, #[serde(default)] pub reported_time: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTOneClickDeviceInfo { - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, #[serde(default)] @@ -39,21 +75,39 @@ pub struct IoTOneClickDeviceInfo { #[serde(default)] pub device_id: Option, pub remaining_life: f64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTOneClickPlacementInfo { #[serde(default)] pub project_name: Option, #[serde(default)] pub placement_name: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub devices: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/iot_button/mod.rs b/lambda-events/src/event/iot_button/mod.rs index 2d2e4627d..75ad7ca31 100644 --- a/lambda-events/src/event/iot_button/mod.rs +++ b/lambda-events/src/event/iot_button/mod.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTButtonEvent { #[serde(default)] @@ -9,6 +15,14 @@ pub struct IoTButtonEvent { pub click_type: Option, #[serde(default)] pub battery_voltage: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/iot_deprecated/mod.rs b/lambda-events/src/event/iot_deprecated/mod.rs index 12c1df990..e29bfab18 100644 --- a/lambda-events/src/event/iot_deprecated/mod.rs +++ b/lambda-events/src/event/iot_deprecated/mod.rs @@ -1,9 +1,15 @@ use crate::iot::*; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `IoTCustomAuthorizerRequest` contains data coming in to a custom IoT device gateway authorizer function. /// Deprecated: Use IoTCoreCustomAuthorizerRequest instead. `IoTCustomAuthorizerRequest` does not correctly model the request schema -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCustomAuthorizerRequest { pub http_context: Option, @@ -14,6 +20,14 @@ pub struct IoTCustomAuthorizerRequest { pub authorization_token: Option, #[serde(default)] pub token_signature: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type IoTHttpContext = IoTCoreHttpContext; @@ -24,7 +38,9 @@ pub type IoTTlsContext = IoTCoreTlsContext; /// `IoTCustomAuthorizerResponse` represents the expected format of an IoT device gateway authorization response. /// Deprecated: Use IoTCoreCustomAuthorizerResponse. `IoTCustomAuthorizerResponse` does not correctly model the response schema. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct IoTCustomAuthorizerResponse { pub is_authenticated: bool, @@ -33,4 +49,12 @@ pub struct IoTCustomAuthorizerResponse { pub disconnect_after_in_seconds: i32, pub refresh_after_in_seconds: i32, pub policy_documents: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/kafka/mod.rs b/lambda-events/src/event/kafka/mod.rs index 27a1e9218..5ca28b3db 100644 --- a/lambda-events/src/event/kafka/mod.rs +++ b/lambda-events/src/event/kafka/mod.rs @@ -1,22 +1,38 @@ -use crate::{custom_serde::deserialize_lambda_map, encodings::MillisecondTimestamp}; +use crate::{custom_serde::deserialize_nullish, encodings::MillisecondTimestamp}; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaEvent { #[serde(default)] pub event_source: Option, #[serde(default)] pub event_source_arn: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub records: HashMap>, #[serde(default)] pub bootstrap_servers: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KafkaRecord { #[serde(default)] @@ -29,6 +45,14 @@ pub struct KafkaRecord { pub key: Option, pub value: Option, pub headers: Vec>>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/kinesis/analytics.rs b/lambda-events/src/event/kinesis/analytics.rs index 74c956068..ae4381135 100644 --- a/lambda-events/src/event/kinesis/analytics.rs +++ b/lambda-events/src/event/kinesis/analytics.rs @@ -1,6 +1,12 @@ use crate::encodings::Base64Data; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisAnalyticsOutputDeliveryEvent { @@ -9,22 +15,52 @@ pub struct KinesisAnalyticsOutputDeliveryEvent { #[serde(default)] pub application_arn: Option, pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisAnalyticsOutputDeliveryEventRecord { #[serde(default)] pub record_id: Option, pub data: Base64Data, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisAnalyticsOutputDeliveryResponse { pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisAnalyticsOutputDeliveryResponseRecord { @@ -33,4 +69,12 @@ pub struct KinesisAnalyticsOutputDeliveryResponseRecord { /// possible values include Ok and DeliveryFailed #[serde(default)] pub result: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/kinesis/event.rs b/lambda-events/src/event/kinesis/event.rs index fac80e07d..09b54eb40 100644 --- a/lambda-events/src/event/kinesis/event.rs +++ b/lambda-events/src/event/kinesis/event.rs @@ -2,18 +2,34 @@ use crate::{ encodings::{Base64Data, SecondTimestamp}, time_window::{TimeWindowEventResponseProperties, TimeWindowProperties}, }; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Default, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisEvent { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `KinesisTimeWindowEvent` represents an Amazon Dynamodb event when using time windows -/// ref. https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +/// ref. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisTimeWindowEvent { #[serde(rename = "KinesisEvent")] @@ -22,9 +38,19 @@ pub struct KinesisTimeWindowEvent { #[serde(rename = "TimeWindowProperties")] #[serde(flatten)] pub time_window_properties: TimeWindowProperties, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `KinesisTimeWindowEventResponse` is the outer structure to report batch item failures for KinesisTimeWindowEvent. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisTimeWindowEventResponse { @@ -32,9 +58,19 @@ pub struct KinesisTimeWindowEventResponse { #[serde(flatten)] pub time_window_event_response_properties: TimeWindowEventResponseProperties, // pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisEventRecord { /// nolint: stylecheck @@ -57,9 +93,19 @@ pub struct KinesisEventRecord { #[serde(default)] pub invoke_identity_arn: Option, pub kinesis: KinesisRecord, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisRecord { pub approximate_arrival_timestamp: SecondTimestamp, @@ -72,8 +118,17 @@ pub struct KinesisRecord { pub sequence_number: String, #[serde(default)] pub kinesis_schema_version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum KinesisEncryptionType { @@ -109,4 +164,17 @@ mod test { let reparsed: KinesisEvent = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + /// `cargo lambda init` autogenerates code that relies on `Default` being implemented for event structs. + /// + /// This test validates that `Default` is implemented for each KinesisEvent struct. + #[test] + #[cfg(feature = "kinesis")] + fn test_ensure_default_implemented_for_structs() { + let _kinesis_event = KinesisEvent::default(); + let _kinesis_time_window_event = KinesisTimeWindowEvent::default(); + let _kinesis_event_record = KinesisEventRecord::default(); + let _kinesis_record = KinesisRecord::default(); + let _kinesis_encryption_type = KinesisEncryptionType::default(); + } } diff --git a/lambda-events/src/event/lambda_function_urls/mod.rs b/lambda-events/src/event/lambda_function_urls/mod.rs index 37ddfe39b..b08700a77 100644 --- a/lambda-events/src/event/lambda_function_urls/mod.rs +++ b/lambda-events/src/event/lambda_function_urls/mod.rs @@ -1,11 +1,17 @@ +#[cfg(feature = "builders")] +use bon::Builder; use http::HeaderMap; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::{deserialize_lambda_map, serialize_headers}; +use crate::custom_serde::{deserialize_nullish, serialize_headers}; /// `LambdaFunctionUrlRequest` contains data coming from the HTTP request to a Lambda Function URL. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlRequest { /// Version is expected to be `"2.0"` @@ -19,16 +25,26 @@ pub struct LambdaFunctionUrlRequest { #[serde(deserialize_with = "http_serde::header_map::deserialize", default)] #[serde(serialize_with = "serialize_headers")] pub headers: HeaderMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub query_string_parameters: HashMap, pub request_context: LambdaFunctionUrlRequestContext, pub body: Option, pub is_base64_encoded: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LambdaFunctionUrlRequestContext` contains the information to identify the AWS account and resources invoking the Lambda function. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlRequestContext { #[serde(default)] @@ -50,17 +66,37 @@ pub struct LambdaFunctionUrlRequestContext { pub time: Option, pub time_epoch: i64, pub http: LambdaFunctionUrlRequestContextHttpDescription, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LambdaFunctionUrlRequestContextAuthorizerDescription` contains authorizer information for the request context. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlRequestContextAuthorizerDescription { pub iam: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LambdaFunctionUrlRequestContextAuthorizerIamDescription` contains IAM information for the request context. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlRequestContextAuthorizerIamDescription { #[serde(default)] @@ -73,10 +109,20 @@ pub struct LambdaFunctionUrlRequestContextAuthorizerIamDescription { pub user_arn: Option, #[serde(default)] pub user_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LambdaFunctionUrlRequestContextHttpDescription` contains HTTP information for the request context. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlRequestContextHttpDescription { #[serde(default)] @@ -89,10 +135,20 @@ pub struct LambdaFunctionUrlRequestContextHttpDescription { pub source_ip: Option, #[serde(default)] pub user_agent: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `LambdaFunctionUrlResponse` configures the HTTP response to be returned by Lambda Function URL for the request. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LambdaFunctionUrlResponse { pub status_code: i64, @@ -103,4 +159,12 @@ pub struct LambdaFunctionUrlResponse { pub body: Option, pub is_base64_encoded: bool, pub cookies: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/lex/mod.rs b/lambda-events/src/event/lex/mod.rs index d8f9403cb..88c48c58f 100644 --- a/lambda-events/src/event/lex/mod.rs +++ b/lambda-events/src/event/lex/mod.rs @@ -1,9 +1,15 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexEvent { pub message_version: Option, @@ -11,7 +17,7 @@ pub struct LexEvent { pub user_id: Option, pub input_transcript: Option, pub session_attributes: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub request_attributes: HashMap, pub bot: Option, @@ -20,81 +26,161 @@ pub struct LexEvent { pub alternative_intents: Option>, /// Deprecated: the DialogAction field is never populated by Lex events pub dialog_action: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexBot { pub name: Option, pub alias: Option, pub version: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexCurrentIntent { pub name: Option, pub nlu_intent_confidence_score: Option, pub slots: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub slot_details: HashMap, pub confirmation_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexAlternativeIntents { pub name: Option, pub nlu_intent_confidence_score: Option, pub slots: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub slot_details: HashMap, pub confirmation_status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SlotDetail { pub resolutions: Option>>, pub original_value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexDialogAction { pub type_: Option, pub fulfillment_state: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message: HashMap, pub intent_name: Option, pub slots: Option, pub slot_to_elicit: Option, pub response_card: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type SessionAttributes = HashMap; pub type Slots = HashMap>; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexResponse { pub session_attributes: SessionAttributes, pub dialog_action: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LexResponseCard { pub version: Option, pub content_type: Option, pub generic_attachments: Option>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Attachment { pub title: Option, @@ -102,6 +188,14 @@ pub struct Attachment { pub image_url: Option, pub attachment_link_url: Option, pub buttons: Option>>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/mod.rs b/lambda-events/src/event/mod.rs index d63acc4d4..cfd4c9e85 100644 --- a/lambda-events/src/event/mod.rs +++ b/lambda-events/src/event/mod.rs @@ -1,162 +1,213 @@ /// AWS Lambda event definitions for activemq. #[cfg(feature = "activemq")] +#[cfg_attr(docsrs, doc(cfg(feature = "activemq")))] pub mod activemq; /// AWS Lambda event definitions for alb. #[cfg(feature = "alb")] +#[cfg_attr(docsrs, doc(cfg(feature = "alb")))] pub mod alb; /// AWS Lambda event definitions for apigw. #[cfg(feature = "apigw")] +#[cfg_attr(docsrs, doc(cfg(feature = "apigw")))] pub mod apigw; /// AWS Lambda event definitions for appsync. #[cfg(feature = "appsync")] +#[cfg_attr(docsrs, doc(cfg(feature = "appsync")))] pub mod appsync; /// AWS Lambda event definitions for autoscaling. #[cfg(feature = "autoscaling")] +#[cfg_attr(docsrs, doc(cfg(feature = "autoscaling")))] pub mod autoscaling; /// AWS Lambda event definitions for agent for amazon bedrock #[cfg(feature = "bedrock_agent_runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "bedrock_agent_runtime")))] pub mod bedrock_agent_runtime; /// AWS Lambda event definitions for chime_bot. #[cfg(feature = "chime_bot")] +#[cfg_attr(docsrs, doc(cfg(feature = "chime_bot")))] pub mod chime_bot; /// AWS Lambda event definitions for clientvpn. #[cfg(feature = "clientvpn")] +#[cfg_attr(docsrs, doc(cfg(feature = "clientvpn")))] pub mod clientvpn; /// AWS Lambda event definitions for cloudformation. #[cfg(feature = "cloudformation")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudformation")))] pub mod cloudformation; /// AWS Lambda event definitions for CloudWatch alarms. #[cfg(feature = "cloudwatch_alarms")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_alarms")))] pub mod cloudwatch_alarms; /// AWS Lambda event definitions for CloudWatch events. #[cfg(feature = "cloudwatch_events")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_events")))] pub mod cloudwatch_events; /// AWS Lambda event definitions for cloudwatch_logs. #[cfg(feature = "cloudwatch_logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_logs")))] pub mod cloudwatch_logs; /// AWS Lambda event definitions for code_commit. #[cfg(feature = "code_commit")] +#[cfg_attr(docsrs, doc(cfg(feature = "code_commit")))] pub mod code_commit; /// AWS Lambda event definitions for codebuild. #[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] pub mod codebuild; /// AWS Lambda event definitions for codedeploy. #[cfg(feature = "codedeploy")] +#[cfg_attr(docsrs, doc(cfg(feature = "codedeploy")))] pub mod codedeploy; /// AWS Lambda event definitions for codepipeline_cloudwatch. #[cfg(feature = "codepipeline_cloudwatch")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_cloudwatch")))] pub mod codepipeline_cloudwatch; /// AWS Lambda event definitions for codepipeline_job. #[cfg(feature = "codepipeline_job")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_job")))] pub mod codepipeline_job; /// AWS Lambda event definitions for cognito. #[cfg(feature = "cognito")] +#[cfg_attr(docsrs, doc(cfg(feature = "cognito")))] pub mod cognito; /// AWS Lambda event definitions for config. #[cfg(feature = "config")] +#[cfg_attr(docsrs, doc(cfg(feature = "config")))] pub mod config; /// AWS Lambda event definitions for connect. #[cfg(feature = "connect")] +#[cfg_attr(docsrs, doc(cfg(feature = "connect")))] pub mod connect; +/// AWS Lambda event definitions for AWS Control Tower lifecycle events. +#[cfg(feature = "controltower")] +#[cfg_attr(docsrs, doc(cfg(feature = "controltower")))] +pub mod controltower; + /// AWS Lambda event definitions for dynamodb. #[cfg(feature = "dynamodb")] +#[cfg_attr(docsrs, doc(cfg(feature = "dynamodb")))] pub mod dynamodb; /// AWS Lambda event definitions for ecr_scan. #[cfg(feature = "ecr_scan")] +#[cfg_attr(docsrs, doc(cfg(feature = "ecr_scan")))] pub mod ecr_scan; /// AWS Lambda event definitions for firehose. #[cfg(feature = "firehose")] +#[cfg_attr(docsrs, doc(cfg(feature = "firehose")))] pub mod firehose; /// AWS Lambda event definitions for iam. #[cfg(feature = "iam")] +#[cfg_attr(docsrs, doc(cfg(feature = "iam")))] pub mod iam; /// AWS Lambda event definitions for iot. #[cfg(feature = "iot")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot")))] pub mod iot; /// AWS Lambda event definitions for iot_1_click. #[cfg(feature = "iot_1_click")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_1_click")))] pub mod iot_1_click; /// AWS Lambda event definitions for iot_button. #[cfg(feature = "iot_button")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_button")))] pub mod iot_button; /// AWS Lambda event definitions for iot_deprecated. #[cfg(feature = "iot_deprecated")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_deprecated")))] pub mod iot_deprecated; /// AWS Lambda event definitions for kafka. #[cfg(feature = "kafka")] +#[cfg_attr(docsrs, doc(cfg(feature = "kafka")))] pub mod kafka; /// AWS Lambda event definitions for kinesis. #[cfg(feature = "kinesis")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis")))] pub mod kinesis; /// AWS Lambda event definitions for lambda_function_urls. #[cfg(feature = "lambda_function_urls")] +#[cfg_attr(docsrs, doc(cfg(feature = "lambda_function_urls")))] pub mod lambda_function_urls; /// AWS Lambda event definitions for lex. #[cfg(feature = "lex")] +#[cfg_attr(docsrs, doc(cfg(feature = "lex")))] pub mod lex; /// AWS Lambda event definitions for rabbitmq. #[cfg(feature = "rabbitmq")] +#[cfg_attr(docsrs, doc(cfg(feature = "rabbitmq")))] pub mod rabbitmq; /// AWS Lambda event definitions for s3. #[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] pub mod s3; /// AWS Lambda event definitions for secretsmanager. #[cfg(feature = "secretsmanager")] +#[cfg_attr(docsrs, doc(cfg(feature = "secretsmanager")))] pub mod secretsmanager; /// AWS Lambda event definitions for ses. #[cfg(feature = "ses")] +#[cfg_attr(docsrs, doc(cfg(feature = "ses")))] pub mod ses; /// AWS Lambda event definitions for SNS. #[cfg(feature = "sns")] +#[cfg_attr(docsrs, doc(cfg(feature = "sns")))] pub mod sns; /// AWS Lambda event definitions for SQS. #[cfg(feature = "sqs")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqs")))] pub mod sqs; /// AWS Lambda event definitions for streams. #[cfg(feature = "streams")] +#[cfg_attr(docsrs, doc(cfg(feature = "streams")))] pub mod streams; // AWS Lambda event definitions for DocumentDB #[cfg(feature = "documentdb")] +#[cfg_attr(docsrs, doc(cfg(feature = "documentdb")))] pub mod documentdb; /// AWS Lambda event definitions for EventBridge. #[cfg(feature = "eventbridge")] +#[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub mod eventbridge; + +/// AWS Lambda event definitions for VPC Lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub mod vpc_lattice; diff --git a/lambda-events/src/event/rabbitmq/mod.rs b/lambda-events/src/event/rabbitmq/mod.rs index 233272762..383f93477 100644 --- a/lambda-events/src/event/rabbitmq/mod.rs +++ b/lambda-events/src/event/rabbitmq/mod.rs @@ -1,32 +1,56 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RabbitMqEvent { #[serde(default)] pub event_source: Option, #[serde(default)] pub event_source_arn: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(rename = "rmqMessagesByQueue")] pub messages_by_queue: HashMap>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RabbitMqMessage { pub basic_properties: RabbitMqBasicProperties, #[serde(default)] pub data: Option, pub redelivered: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RabbitMqBasicProperties where @@ -37,7 +61,7 @@ where pub content_type: Option, pub content_encoding: Option, /// Application or header exchange table - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] #[serde(bound = "")] pub headers: HashMap, @@ -56,6 +80,14 @@ where pub app_id: Option, pub cluster_id: Option, pub body_size: u64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/s3/batch_job.rs b/lambda-events/src/event/s3/batch_job.rs index e3eb691e4..a6d3b12a6 100644 --- a/lambda-events/src/event/s3/batch_job.rs +++ b/lambda-events/src/event/s3/batch_job.rs @@ -1,6 +1,12 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `S3BatchJobEvent` encapsulates the detail of a s3 batch job +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3BatchJobEvent { @@ -10,17 +16,37 @@ pub struct S3BatchJobEvent { pub invocation_id: Option, pub job: S3BatchJob, pub tasks: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `S3BatchJob` whichs have the job id +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3BatchJob { #[serde(default)] pub id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `S3BatchJobTask` represents one task in the s3 batch job and have all task details +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3BatchJobTask { @@ -32,9 +58,19 @@ pub struct S3BatchJobTask { pub s3_version_id: Option, #[serde(default)] pub s3_bucket_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `S3BatchJobResponse` is the response of a iven s3 batch job with the results +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3BatchJobResponse { @@ -45,9 +81,19 @@ pub struct S3BatchJobResponse { #[serde(default)] pub invocation_id: Option, pub results: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `S3BatchJobResult` represents the result of a given task +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3BatchJobResult { @@ -57,4 +103,12 @@ pub struct S3BatchJobResult { pub result_code: Option, #[serde(default)] pub result_string: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } diff --git a/lambda-events/src/event/s3/event.rs b/lambda-events/src/event/s3/event.rs index e062c7d23..f2d69165a 100644 --- a/lambda-events/src/event/s3/event.rs +++ b/lambda-events/src/event/s3/event.rs @@ -1,18 +1,34 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; /// `S3Event` which wrap an array of `S3Event`Record +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3Event { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `S3EventRecord` which wrap record data +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3EventRecord { @@ -28,27 +44,57 @@ pub struct S3EventRecord { #[serde(rename = "userIdentity")] pub principal_id: S3UserIdentity, pub request_parameters: S3RequestParameters, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub response_elements: HashMap, pub s3: S3Entity, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3UserIdentity { #[serde(default)] pub principal_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3RequestParameters { #[serde(default)] #[serde(rename = "sourceIPAddress")] pub source_ip_address: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3Entity { @@ -59,8 +105,18 @@ pub struct S3Entity { pub configuration_id: Option, pub bucket: S3Bucket, pub object: S3Object, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3Bucket { @@ -70,8 +126,18 @@ pub struct S3Bucket { pub owner_identity: Option, #[serde(default)] pub arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3Object { @@ -86,6 +152,14 @@ pub struct S3Object { pub e_tag: Option, #[serde(default)] pub sequencer: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/s3/object_lambda.rs b/lambda-events/src/event/s3/object_lambda.rs index 738cd72cc..8464fcae9 100644 --- a/lambda-events/src/event/s3/object_lambda.rs +++ b/lambda-events/src/event/s3/object_lambda.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "builders")] +use bon::Builder; use http::HeaderMap; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; @@ -6,7 +8,9 @@ use std::collections::HashMap; use crate::custom_serde::{deserialize_headers, serialize_headers}; /// `S3ObjectLambdaEvent` contains data coming from S3 object lambdas -/// See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html +/// See: +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct S3ObjectLambdaEvent

@@ -24,43 +28,93 @@ where pub user_request: UserRequest, pub user_identity: UserIdentity, pub protocol_version: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `GetObjectContext` contains the input and output details /// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetObjectContext { pub input_s3_url: String, pub output_route: String, pub output_token: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `HeadObjectContext` /// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct HeadObjectContext { pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ListObjectsContext` /// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ListObjectsContext { pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `ListObjectsV2Context` /// for connections to Amazon S3 and S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct ListObjectsV2Context { pub input_s3_url: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `Configuration` contains information about the Object Lambda access point +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Configuration

@@ -72,9 +126,19 @@ where pub supporting_access_point_arn: String, #[serde(default, bound = "")] pub payload: P, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `UserRequest` contains information about the original call to S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserRequest { @@ -82,9 +146,19 @@ pub struct UserRequest { #[serde(deserialize_with = "deserialize_headers", default)] #[serde(serialize_with = "serialize_headers")] pub headers: HeaderMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `UserIdentity` contains details about the identity that made the call to S3 Object Lambda +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserIdentity { @@ -94,16 +168,36 @@ pub struct UserIdentity { pub account_id: String, pub access_key_id: String, pub session_context: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SessionContext { pub attributes: HashMap, #[serde(default)] pub session_issuer: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SessionIssuer { @@ -112,6 +206,14 @@ pub struct SessionIssuer { pub arn: String, pub account_id: String, pub user_name: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/secretsmanager/mod.rs b/lambda-events/src/event/secretsmanager/mod.rs index 3ed8d2389..0507467f8 100644 --- a/lambda-events/src/event/secretsmanager/mod.rs +++ b/lambda-events/src/event/secretsmanager/mod.rs @@ -1,11 +1,25 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct SecretsManagerSecretRotationEvent { pub step: String, pub secret_id: String, pub client_request_token: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] diff --git a/lambda-events/src/event/ses/mod.rs b/lambda-events/src/event/ses/mod.rs index 2a60957ab..0b8bec55e 100644 --- a/lambda-events/src/event/ses/mod.rs +++ b/lambda-events/src/event/ses/mod.rs @@ -1,15 +1,31 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `SimpleEmailEvent` is the outer structure of an event sent via SES. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailEvent { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailRecord { #[serde(default)] @@ -17,17 +33,37 @@ pub struct SimpleEmailRecord { #[serde(default)] pub event_source: Option, pub ses: SimpleEmailService, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailService { pub mail: SimpleEmailMessage, pub receipt: SimpleEmailReceipt, pub content: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailMessage { pub common_headers: SimpleEmailCommonHeaders, @@ -39,9 +75,19 @@ pub struct SimpleEmailMessage { pub headers_truncated: bool, #[serde(default)] pub message_id: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailReceipt { pub recipients: Vec, @@ -55,21 +101,42 @@ pub struct SimpleEmailReceipt { pub virus_verdict: SimpleEmailVerdict, pub action: SimpleEmailReceiptAction, pub processing_time_millis: i64, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailHeader { #[serde(default)] pub name: Option, #[serde(default)] pub value: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailCommonHeaders { pub from: Vec, + #[serde(default)] pub to: Vec, #[serde(default)] pub return_path: Option, @@ -79,13 +146,23 @@ pub struct SimpleEmailCommonHeaders { pub date: Option, #[serde(default)] pub subject: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `SimpleEmailReceiptAction` is a logical union of fields present in all action /// Types. For example, the FunctionARN and InvocationType fields are only /// present for the Lambda Type, and the BucketName and ObjectKey fields are only /// present for the S3 Type. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailReceiptAction { #[serde(default)] @@ -100,22 +177,50 @@ pub struct SimpleEmailReceiptAction { pub invocation_type: Option, pub function_arn: Option, pub organization_arn: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailVerdict { #[serde(default)] pub status: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } pub type SimpleEmailDispositionValue = String; /// `SimpleEmailDisposition` disposition return for SES to control rule functions -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SimpleEmailDisposition { pub disposition: SimpleEmailDispositionValue, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] @@ -151,4 +256,14 @@ mod test { let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "ses")] + fn example_ses_bcc_event() { + let data = include_bytes!("../../fixtures/example-ses-bcc-event.json"); + let parsed: SimpleEmailEvent = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SimpleEmailEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } } diff --git a/lambda-events/src/event/sns/mod.rs b/lambda-events/src/event/sns/mod.rs index 0fda569d1..5879bc873 100644 --- a/lambda-events/src/event/sns/mod.rs +++ b/lambda-events/src/event/sns/mod.rs @@ -1,20 +1,37 @@ +#[cfg(feature = "builders")] +use bon::Builder; use chrono::{DateTime, Utc}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; -use crate::custom_serde::{deserialize_lambda_map, deserialize_nullish_boolean}; +use crate::custom_serde::deserialize_nullish; /// The `Event` notification event handled by Lambda /// /// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct SnsEvent { pub records: Vec, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// SnsRecord stores information about each record of a SNS event -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct SnsRecord { /// A string containing the event source. @@ -28,10 +45,25 @@ pub struct SnsRecord { /// An SNS object representing the SNS message. pub sns: SnsMessage, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -/// SnsMessage stores information about each record of a SNS event -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +/// SnsMessage stores information about SNS **Notification** type messages only. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct SnsMessage { /// The type of SNS message. For a lambda event, this should always be **Notification** @@ -73,23 +105,134 @@ pub struct SnsMessage { pub message: String, /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// SnsSubscriptionMessage stores information about SNS SubscriptionConfirmation and +/// UnsubscribeConfirmation type messages. +/// +/// Use this struct when handling messages where the `Type` field equals "SubscriptionConfirmation" +/// or "UnsubscribeConfirmation". For handling Notification messages, use [`SnsMessage`] instead. +/// +/// # Distinguishing SubscriptionConfirmation from UnsubscribeConfirmation +/// +/// Both message types use this same struct. You can distinguish them by: +/// - Checking the `sns_message_type` field ("SubscriptionConfirmation" or "UnsubscribeConfirmation") +/// - Checking `subscribe_url`: `Some(url)` for SubscriptionConfirmation, `None` for UnsubscribeConfirmation +/// +/// # Example +/// +/// ``` +/// use aws_lambda_events::event::sns::SnsSubscriptionMessage; +/// +/// fn handle_confirmation(msg: SnsSubscriptionMessage) { +/// if let Some(url) = &msg.subscribe_url { +/// // SubscriptionConfirmation - visit URL or use token to confirm +/// println!("Confirm subscription at: {}", url); +/// } else { +/// // UnsubscribeConfirmation +/// println!("Unsubscribe confirmed"); +/// } +/// } +/// ``` +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "PascalCase")] +pub struct SnsSubscriptionMessage { + /// The type of SNS message. Will be "SubscriptionConfirmation" or "UnsubscribeConfirmation". + #[serde(rename = "Type")] + pub sns_message_type: String, + + /// A Universally Unique Identifier, unique for each message published. + pub message_id: String, + + /// The Amazon Resource Name (ARN) for the topic that this message was published to. + pub topic_arn: String, + + /// The Subject parameter specified when the notification was published to the topic. + #[serde(default)] + pub subject: Option, + + /// The time (UTC) when the message was sent. + pub timestamp: DateTime, + + /// Version of the Amazon SNS signature used. + pub signature_version: String, + + /// Base64-encoded SHA1withRSA signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values. + pub signature: String, + + /// The URL to the certificate that was used to sign the message. + #[serde(alias = "SigningCertURL")] + pub signing_cert_url: String, + + /// A URL that you can visit to confirm the subscription. Present only for SubscriptionConfirmation messages. + /// + /// For UnsubscribeConfirmation messages, this field will be `None`. + #[serde(alias = "SubscribeURL")] + #[serde(default)] + pub subscribe_url: Option, + + /// A value you can use with the ConfirmSubscription action to confirm the subscription. + /// Alternatively, you can simply visit the `subscribe_url`. + #[serde(rename = "Token")] + pub token: String, + + /// The Message value containing a description of the subscription confirmation. + pub message: String, + + /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) + #[serde(deserialize_with = "deserialize_nullish")] + #[serde(default)] + pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// An alternate `Event` notification event to use alongside `SnsRecordObj` and `SnsMessageObj` if you want to deserialize an object inside your SNS messages rather than getting an `Option` message /// /// [https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html](https://docs.aws.amazon.com/lambda/latest/dg/with-sns.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] #[serde(bound(deserialize = "T: DeserializeOwned"))] pub struct SnsEventObj { pub records: Vec>, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// Alternative to `SnsRecord`, used alongside `SnsEventObj` and `SnsMessageObj` when deserializing nested objects from within SNS messages) -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] #[serde(bound(deserialize = "T: DeserializeOwned"))] pub struct SnsRecordObj { @@ -104,11 +247,26 @@ pub struct SnsRecordObj { /// An SNS object representing the SNS message. pub sns: SnsMessageObj, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj` and `SnsRecordObj` for deserializing the message into a struct of type `T` +/// Alternate version of `SnsMessage` to use in conjunction with `SnsEventObj` and `SnsRecordObj` for deserializing the message into a struct of type `T`. +/// +/// **Important**: This struct is designed specifically for handling SNS Notification messages +/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or +/// UnsubscribeConfirmation messages, use [`SnsSubscriptionMessage`] instead. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[serde_with::serde_as] -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] #[serde(bound(deserialize = "T: DeserializeOwned"))] pub struct SnsMessageObj { @@ -153,9 +311,18 @@ pub struct SnsMessageObj { pub message: T, /// This is a HashMap of defined attributes for a message. Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// Structured metadata items (such as timestamps, geospatial data, signatures, and identifiers) about the message. @@ -163,6 +330,8 @@ pub struct SnsMessageObj { /// Message attributes are optional and separate from—but are sent together with—the message body. The receiver can use this information to decide how to handle the message without having to process the message body first. /// /// Additional details can be found in the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct MessageAttribute { /// The data type of the attribute. Per the [SNS Developer Guide](https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html), lambda notifications, this will only be **String** or **Binary**. @@ -172,8 +341,19 @@ pub struct MessageAttribute { /// The user-specified message attribute value. #[serde(rename = "Value")] pub value: String, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudWatchAlarmPayload { @@ -188,8 +368,18 @@ pub struct CloudWatchAlarmPayload { pub alarm_arn: String, pub old_state_value: String, pub trigger: CloudWatchAlarmTrigger, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudWatchAlarmTrigger { @@ -208,8 +398,18 @@ pub struct CloudWatchAlarmTrigger { pub unit: Option, #[serde(default)] pub dimensions: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudWatchMetricDataQuery { @@ -218,10 +418,20 @@ pub struct CloudWatchMetricDataQuery { pub label: Option, pub metric_stat: Option, pub period: Option, - #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + #[serde(default, deserialize_with = "deserialize_nullish")] pub return_data: bool, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudWatchMetricStat { @@ -229,8 +439,18 @@ pub struct CloudWatchMetricStat { pub period: i64, pub stat: String, pub unit: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct CloudWatchMetric { @@ -238,12 +458,30 @@ pub struct CloudWatchMetric { pub dimensions: Vec, pub metric_name: Option, pub namespace: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct CloudWatchDimension { pub name: String, pub value: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] @@ -312,7 +550,7 @@ mod test { } let parsed: SnsEventObj = serde_json::from_slice(data).unwrap(); - println!("{:?}", parsed); + println!("{parsed:?}"); assert_eq!(parsed.records[0].sns.message.foo, "Hello world!"); assert_eq!(parsed.records[0].sns.message.bar, 123); @@ -321,4 +559,40 @@ mod test { let reparsed: SnsEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_subscription_confirmation() { + // Test for issue #966: SnsSubscriptionMessage for SubscriptionConfirmation types + let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("SubscriptionConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_some()); + assert_eq!( + "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + parsed.subscribe_url.as_ref().unwrap() + ); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbffff", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_unsubscribe_confirmation() { + // Test for UnsubscribeConfirmation messages - subscribe_url should be None + let data = include_bytes!("../../fixtures/example-sns-unsubscribe-confirmation.json"); + let parsed: SnsSubscriptionMessage = serde_json::from_slice(data).unwrap(); + + assert_eq!("UnsubscribeConfirmation", parsed.sns_message_type); + assert!(parsed.subscribe_url.is_none()); + assert_eq!("2336412f37fb687f5d51e6e2425dacbbeeee", parsed.token); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsSubscriptionMessage = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } } diff --git a/lambda-events/src/event/sqs/mod.rs b/lambda-events/src/event/sqs/mod.rs index 9dd69f66a..3209e0f58 100644 --- a/lambda-events/src/event/sqs/mod.rs +++ b/lambda-events/src/event/sqs/mod.rs @@ -1,16 +1,32 @@ -use crate::{custom_serde::deserialize_lambda_map, encodings::Base64Data}; +use crate::{custom_serde::deserialize_nullish, encodings::Base64Data}; +#[cfg(feature = "builders")] +use bon::Builder; use serde::{de::DeserializeOwned, Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; use std::collections::HashMap; /// The Event sent to Lambda from SQS. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsEvent { #[serde(rename = "Records")] pub records: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[cfg_attr(feature = "builders", builder(default))] + #[serde(flatten)] + pub other: serde_json::Map, } /// An individual SQS Message, its metadata, and Message Attributes +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsMessage { @@ -25,10 +41,10 @@ pub struct SqsMessage { pub md5_of_body: Option, #[serde(default)] pub md5_of_message_attributes: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, #[serde(default)] @@ -38,9 +54,19 @@ pub struct SqsMessage { pub event_source: Option, #[serde(default)] pub aws_region: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// Alternative to `SqsEvent` to be used alongside `SqsMessageObj` when you need to deserialize a nested object into a struct of type `T` within the SQS Message rather than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] #[serde(bound(deserialize = "T: DeserializeOwned"))] @@ -48,9 +74,19 @@ pub struct SqsEventObj { #[serde(rename = "Records")] #[serde(bound(deserialize = "T: DeserializeOwned"))] pub records: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// Alternative to `SqsMessage` to be used alongside `SqsEventObj` when you need to deserialize a nested object into a struct of type `T` within the SQS Message rather than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[serde_with::serde_as] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(bound(deserialize = "T: DeserializeOwned"))] @@ -70,10 +106,10 @@ pub struct SqsMessageObj { pub md5_of_body: Option, #[serde(default)] pub md5_of_message_attributes: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, #[serde(default)] @@ -83,8 +119,18 @@ pub struct SqsMessageObj { pub event_source: Option, #[serde(default)] pub aws_region: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsMessageAttribute { @@ -96,39 +142,201 @@ pub struct SqsMessageAttribute { pub binary_list_values: Vec, #[serde(default)] pub data_type: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsBatchResponse { pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } +impl SqsBatchResponse { + /// Add a failed message ID to the batch response. + /// + /// When processing SQS messages in batches, you can use this helper method to + /// register individual message failures. Lambda will automatically return failed + /// messages to the queue for reprocessing while successfully processed messages + /// will be deleted. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`BatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's SQS event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + /// + /// # Example + /// + /// ```rust + /// use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse}; + /// use lambda_runtime::{service_fn, Error, LambdaEvent}; + /// + /// async fn function_handler( + /// event: LambdaEvent, + /// ) -> Result { + /// // Start from a default response + /// let mut response = SqsBatchResponse::default(); + /// + /// for record in event.payload.records { + /// let message_id = record.message_id.clone().unwrap_or_default(); + /// + /// // Try to process the message + /// if let Err(e) = process_record(&record).await { + /// println!("Failed to process message {}: {}", message_id, e); + /// + /// // Use the helper to register the failure + /// response.add_failure(message_id); + /// } + /// } + /// + /// Ok(response) + /// } + /// + /// async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), Error> { + /// // Your message processing logic here + /// Ok(()) + /// } + /// ``` + pub fn add_failure(&mut self, message_id: impl Into) { + self.batch_item_failures.push(BatchItemFailure { + item_identifier: message_id.into(), + ..Default::default() + }); + } + + /// Set multiple failed message IDs at once. + /// + /// This is a convenience method for setting all batch item failures in one call. + /// It replaces any previously registered failures. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`BatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's SQS event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + /// + /// # Example + /// + /// ```rust + /// use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse}; + /// use lambda_runtime::{service_fn, Error, LambdaEvent}; + /// + /// async fn function_handler( + /// event: LambdaEvent, + /// ) -> Result { + /// let mut failed_ids = Vec::new(); + /// + /// for record in event.payload.records { + /// let message_id = record.message_id.clone().unwrap_or_default(); + /// + /// // Try to process the message + /// if let Err(e) = process_record(&record).await { + /// println!("Failed to process message {}: {}", message_id, e); + /// failed_ids.push(message_id); + /// } + /// } + /// + /// // Set all failures at once + /// let mut response = SqsBatchResponse::default(); + /// response.set_failures(failed_ids); + /// + /// Ok(response) + /// } + /// + /// async fn process_record(record: &aws_lambda_events::event::sqs::SqsMessage) -> Result<(), Error> { + /// // Your message processing logic here + /// Ok(()) + /// } + /// ``` + pub fn set_failures(&mut self, message_ids: I) + where + I: IntoIterator, + S: Into, + { + self.batch_item_failures = message_ids + .into_iter() + .map(|id| BatchItemFailure { + item_identifier: id.into(), + ..Default::default() + }) + .collect(); + } +} + +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct BatchItemFailure { pub item_identifier: String, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// The Event sent to Lambda from the SQS API. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] #[serde(bound(deserialize = "T: DeserializeOwned"))] pub struct SqsApiEventObj { #[serde(bound(deserialize = "T: DeserializeOwned"))] pub messages: Vec>, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// The Event sent to Lambda from SQS API. Contains 1 or more individual SQS Messages +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsApiEvent { pub messages: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } -/// Alternative to SqsApiEvent to be used alongside SqsApiMessageObj when you need to +/// Alternative to SqsApiEvent to be used alongside `SqsApiMessageObj` when you need to /// deserialize a nested object into a struct of type T within the SQS Message rather /// than just using the raw SQS Message string +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[serde_with::serde_as] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(bound(deserialize = "T: DeserializeOwned"))] @@ -147,15 +355,25 @@ pub struct SqsApiMessageObj { pub md5_of_body: Option, #[serde(default)] pub md5_of_message_attributes: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// An individual SQS API Message, its metadata, and Message Attributes +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "PascalCase")] pub struct SqsApiMessage { @@ -170,12 +388,20 @@ pub struct SqsApiMessage { pub md5_of_body: Option, #[serde(default)] pub md5_of_message_attributes: Option, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub attributes: HashMap, - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub message_attributes: HashMap, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } #[cfg(test)] @@ -245,4 +471,33 @@ mod test { let reparsed: SqsApiEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_batch_response_add_failure() { + let mut response = SqsBatchResponse::default(); + response.add_failure("msg-1".to_string()); + response.add_failure("msg-2".to_string()); + + assert_eq!(response.batch_item_failures.len(), 2); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-1"); + assert_eq!(response.batch_item_failures[1].item_identifier, "msg-2"); + } + + #[test] + #[cfg(feature = "sqs")] + fn example_sqs_batch_response_set_failures() { + let mut response = SqsBatchResponse::default(); + response.set_failures(vec!["msg-1", "msg-2", "msg-3"]); + + assert_eq!(response.batch_item_failures.len(), 3); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-1"); + assert_eq!(response.batch_item_failures[1].item_identifier, "msg-2"); + assert_eq!(response.batch_item_failures[2].item_identifier, "msg-3"); + + // Test that set_failures replaces existing failures + response.set_failures(vec!["msg-4".to_string()]); + assert_eq!(response.batch_item_failures.len(), 1); + assert_eq!(response.batch_item_failures[0].item_identifier, "msg-4"); + } } diff --git a/lambda-events/src/event/streams/mod.rs b/lambda-events/src/event/streams/mod.rs index 9e0fd76fe..b6be8b1df 100644 --- a/lambda-events/src/event/streams/mod.rs +++ b/lambda-events/src/event/streams/mod.rs @@ -1,46 +1,208 @@ +#[cfg(feature = "builders")] +use bon::Builder; use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; /// `KinesisEventResponse` is the outer structure to report batch item failures for KinesisEvent. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisEventResponse { pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +impl KinesisEventResponse { + /// Add a failed item identifier to the batch response. + /// + /// When processing Kinesis records in batches, you can use this helper method to + /// register individual record failures. Lambda will automatically retry failed + /// records while successfully processed records will be checkpointed. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`KinesisBatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's Kinesis event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + pub fn add_failure(&mut self, item_identifier: impl Into) { + self.batch_item_failures.push(KinesisBatchItemFailure { + item_identifier: Some(item_identifier.into()), + #[cfg(feature = "catch-all-fields")] + other: serde_json::Map::new(), + ..Default::default() + }); + } + + /// Set multiple failed item identifiers at once. + /// + /// This is a convenience method for setting all batch item failures in one call. + /// It replaces any previously registered failures. + /// + /// Besides `item_identifiers`, the generated struct will use default field values for [`KinesisBatchItemFailure`]. + /// + /// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures` + /// to be enabled in your Lambda function's Kinesis event source mapping configuration. + /// Without this setting, Lambda will retry the entire batch on any failure. + pub fn set_failures(&mut self, item_identifiers: I) + where + I: IntoIterator, + S: Into, + { + self.batch_item_failures = item_identifiers + .into_iter() + .map(|id| KinesisBatchItemFailure { + item_identifier: Some(id.into()), + #[cfg(feature = "catch-all-fields")] + other: serde_json::Map::new(), + ..Default::default() + }) + .collect(); + } } /// `KinesisBatchItemFailure` is the individual record which failed processing. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct KinesisBatchItemFailure { #[serde(default)] pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `DynamoDbEventResponse` is the outer structure to report batch item failures for DynamoDBEvent. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DynamoDbEventResponse { pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `DynamoDbBatchItemFailure` is the individual record which failed processing. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DynamoDbBatchItemFailure { #[serde(default)] pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `SqsEventResponse` is the outer structure to report batch item failures for SQSEvent. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsEventResponse { pub batch_item_failures: Vec, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, } /// `SqsBatchItemFailure` is the individual record which failed processing. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SqsBatchItemFailure { #[serde(default)] pub item_identifier: Option, + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn kinesis_event_response_add_failure() { + let mut response = KinesisEventResponse::default(); + response.add_failure("seq-1"); + response.add_failure("seq-2".to_string()); + + assert_eq!(response.batch_item_failures.len(), 2); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-1".to_string()) + ); + assert_eq!( + response.batch_item_failures[1].item_identifier, + Some("seq-2".to_string()) + ); + } + + #[test] + fn kinesis_event_response_set_failures() { + let mut response = KinesisEventResponse::default(); + response.set_failures(vec!["seq-1", "seq-2", "seq-3"]); + + assert_eq!(response.batch_item_failures.len(), 3); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-1".to_string()) + ); + assert_eq!( + response.batch_item_failures[1].item_identifier, + Some("seq-2".to_string()) + ); + assert_eq!( + response.batch_item_failures[2].item_identifier, + Some("seq-3".to_string()) + ); + + // Test that set_failures replaces existing failures + response.set_failures(vec!["seq-4".to_string()]); + assert_eq!(response.batch_item_failures.len(), 1); + assert_eq!( + response.batch_item_failures[0].item_identifier, + Some("seq-4".to_string()) + ); + } } diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs new file mode 100644 index 000000000..bdb95aba1 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -0,0 +1,62 @@ +use crate::{ + custom_serde::{deserialize_headers, serialize_headers}, + encodings::Body, +}; +use http::HeaderMap; + +#[cfg(feature = "builders")] +use bon::Builder; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeResponse` configures the response to be returned +/// by VPC Lattice (both V1 and V2) for the request +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeResponse { + // https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#respond-to-service + /// Whether the body is base64 encoded + #[serde(default)] + pub is_base64_encoded: bool, + + /// The HTTP status code for the request + pub status_code: u16, + + /// The Http headers to return + #[serde(deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + #[serde(skip_serializing_if = "HeaderMap::is_empty")] + #[serde(default)] + pub headers: HeaderMap, + + /// The response body + #[serde(default)] + pub body: Option, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_response() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-response.json"); + let parsed: VpcLatticeResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs new file mode 100644 index 000000000..cfefcb75d --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +mod common; +mod v1; +mod v2; + +// re-export types +pub use self::{common::*, v1::*, v2::*}; + +/// A VPC Lattice request that can be either V1 or V2 format. +/// Deserialization tries V2 first (which has a `version` and `requestContext` field), +/// then falls back to V1. +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum VpcLatticeRequest { + V2(VpcLatticeRequestV2), + V1(VpcLatticeRequestV1), +} diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs new file mode 100644 index 000000000..4ab423ffb --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -0,0 +1,106 @@ +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::{ + custom_serde::{ + deserialize_comma_separated_headers, deserialize_nullish, http_method, serialize_comma_separated_headers, + }, + encodings::Body, +}; + +/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format) +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +// we note that V1 requests are snake cased UNLIKE v2 which are camel cased +#[serde(rename_all = "snake_case")] +pub struct VpcLatticeRequestV1 { + /// The url path for the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_path: Option, + + /// The HTTP method of the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub method: Option, + + /// HTTP headers of the request (V1 uses comma-separated strings for multi-values) + #[serde(deserialize_with = "deserialize_comma_separated_headers", default)] + #[serde(serialize_with = "serialize_comma_separated_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (V1 uses the last value passed for multi-values + /// so no special serializer is needed) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + + /// Catchall to catch any additional fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!(Some("/api/product".to_string()), parsed.raw_path); + assert_eq!(Some(Method::POST), parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!( + Body::Text("{\"id\": 5, \"description\": \"TV\"}".to_string()), + parsed.body.unwrap() + ); + assert!(!parsed.is_base64_encoded); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_headers_multi_values() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("abcd", parsed.headers.get_all("multi").iter().next().unwrap()); + assert_eq!("DEF", parsed.headers.get_all("multi").iter().nth(1).unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_query_string_map() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("tv", parsed.query_string_parameters.first("tags").unwrap()); + } +} diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs new file mode 100644 index 000000000..1865a8644 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -0,0 +1,251 @@ +use crate::custom_serde::{deserialize_headers, deserialize_nullish, http_method, serialize_multi_value_headers}; +#[cfg(feature = "builders")] +use bon::Builder; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeRequestV2` contains data coming from VPC Lattice service (V2 format) +/// see: +/// for field definitions. +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2 { + /// The version of the event structure (always "2.0" for V2) + #[serde(default = "default_version")] + pub version: String, + + /// The url path for the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// The HTTP method of the request. + /// Present only if the protocol is HTTP, HTTPS, or gRPC. + #[serde(deserialize_with = "http_method::deserialize_optional")] + #[serde(serialize_with = "http_method::serialize_optional")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub method: Option, + + /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) + #[serde(default, deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish")] + pub is_base64_encoded: bool, + + /// VPC Lattice specific request context + pub request_context: VpcLatticeRequestV2Context, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// VPC Lattice specific request context +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Context { + /// ARN of the service network that delivers the request + pub service_network_arn: String, + + /// ARN of the service that receives the request + pub service_arn: String, + + /// ARN of the target group that receives the request + pub target_group_arn: String, + + /// Identity information for the request. + /// Present only if authentication is configured. + #[serde(default)] + pub identity: Option, + + /// AWS region where the request is processed + pub region: String, + + /// Time of the request in microseconds since epoch + pub time_epoch: String, + + /// Catchall for additional context fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +/// Identity information in VPC Lattice request context +#[non_exhaustive] +#[cfg_attr(feature = "builders", derive(Builder))] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Identity { + /// ARN of the VPC where the request originated + #[serde(default)] + pub source_vpc_arn: Option, + + /// Type of authentication (e.g., "AWS_IAM") + #[serde(rename = "type")] + #[serde(default)] + pub identity_type: Option, + + /// The authenticated principal + #[serde(default)] + pub principal: Option, + + /// Organization ID of the authenticated principal + #[serde(rename = "principalOrgID")] + #[serde(default)] + pub principal_org_id: Option, + + /// Name of the authenticated session + #[serde(default)] + pub session_name: Option, + + /// X.509 certificate fields (for Roles Anywhere) + #[serde(rename = "x509IssuerOu")] + #[serde(default)] + pub x509_issuer_ou: Option, + #[serde(rename = "x509SanDns")] + #[serde(default)] + pub x509_san_dns: Option, + #[serde(rename = "x509SanNameCn")] + #[serde(default)] + pub x509_san_name_cn: Option, + #[serde(rename = "x509SanUri")] + #[serde(default)] + pub x509_san_uri: Option, + #[serde(rename = "x509SubjectCn")] + #[serde(default)] + pub x509_subject_cn: Option, + + /// Catchall for additional identity fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + #[cfg_attr(feature = "builders", builder(default))] + pub other: serde_json::Map, +} + +fn default_version() -> String { + "2.0".to_string() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + + assert_eq!(Some("/health".to_string()), parsed.path); + assert_eq!(Some(Method::GET), parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + + // headers including testing multi-values + let header_multi = parsed.headers.get_all("multi"); + assert_eq!("x", header_multi.iter().next().unwrap()); + assert_eq!("y", header_multi.iter().nth(1).unwrap()); + + // query string including testing multi-values + assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); + let query_multi = parsed.query_string_parameters.all("multi").unwrap(); + assert_eq!(&"a", query_multi.first().unwrap()); + assert_eq!(&"DEF", query_multi.get(1).unwrap()); + assert_eq!(&"g", query_multi.get(2).unwrap()); + + assert!(parsed.body.is_none()); + assert!(!parsed.is_base64_encoded); + + // nested structure testing + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + parsed.request_context.service_arn + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + parsed.request_context.service_network_arn + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + parsed.request_context.target_group_arn + ); + assert_eq!("ap-southeast-2", parsed.request_context.region); + assert_eq!("1724875399456789", parsed.request_context.time_epoch); + + let context = parsed.request_context.identity.as_ref().unwrap(); + + // identity + assert_eq!( + "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + context.clone().source_vpc_arn.unwrap() + ); + assert_eq!("AWS_IAM", context.clone().identity_type.unwrap()); + assert_eq!( + "arn:aws:iam::123456789012:role/service-role/HealthChecker", + context.clone().principal.unwrap() + ); + assert_eq!("o-50dc6c495c0c9188", context.clone().principal_org_id.unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip() { + // our basic example has instances of multi-value headers and multi-value parameters + // so this test covers both those serialization edge cases + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_base64_body() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-base64.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_role_anywhere() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-roles-anywhere.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/fixtures/example-activemq-event-null-properties.json b/lambda-events/src/fixtures/example-activemq-event-null-properties.json new file mode 100644 index 000000000..2ac6fd543 --- /dev/null +++ b/lambda-events/src/fixtures/example-activemq-event-null-properties.json @@ -0,0 +1,25 @@ +{ + "eventSource": "aws:mq", + "eventSourceArn": "arn:aws:mq:us-west-2:533019413397:broker:shask-test:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5", + "messages": [ + { + "messageID": "ID:b-0f5b7522-2b41-4f85-a615-735a4e6d96b5-2.mq.us-west-2.amazonaws.com-34859-1598944546501-4:12:1:1:3", + "messageType": "jms/text-message", + "timestamp": 1599863938941, + "deliveryMode": 1, + "correlationID": "", + "replyTo": "null", + "destination": { + "physicalName": "testQueue" + }, + "redelivered": false, + "type": "", + "expiration": 0, + "priority": 0, + "data": "RW50ZXIgc29tZSB0ZXh0IGhlcmUgZm9yIHRoZSBtZXNzYWdlIGJvZHkuLi4=", + "brokerInTime": 1599863938943, + "brokerOutTime": 1599863938944, + "properties": null + } + ] +} diff --git a/lambda-events/src/fixtures/example-apigw-request-catch-all.json b/lambda-events/src/fixtures/example-apigw-request-catch-all.json new file mode 100644 index 000000000..fe1955f46 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-request-catch-all.json @@ -0,0 +1,137 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": [ + "me" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser", + "otherField": 2345 + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "otherField": "foobar" +} diff --git a/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json new file mode 100644 index 000000000..5702b7fd9 --- /dev/null +++ b/lambda-events/src/fixtures/example-apigw-v2-custom-authorizer-v2-request-null-maps.json @@ -0,0 +1,48 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": ["cookie1", "cookie2"], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": null, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": null, + "stageVariables": null +} + diff --git a/lambda-events/src/fixtures/example-appsync-direct-resolver.json b/lambda-events/src/fixtures/example-appsync-direct-resolver.json new file mode 100644 index 000000000..9a8048765 --- /dev/null +++ b/lambda-events/src/fixtures/example-appsync-direct-resolver.json @@ -0,0 +1,64 @@ +{ + "arguments": { + "input": "foo" + }, + "identity": { + "sourceIp": [ + "x.x.x.x" + ], + "userArn": "arn:aws:iam::123456789012:user/appsync", + "accountId": "666666666666", + "user": "AIDAAAAAAAAAAAAAAAAAA" + }, + "info": { + "fieldName": "greet", + "parentTypeName": "Query", + "selectionSetGraphQL": "", + "selectionSetList": [], + "variables": { + "inputVar": "foo" + } + }, + "prev": null, + "request": { + "domainName": null, + "headers": { + "accept": "application/json, text/plain, */*", + "accept-encoding": "gzip, deflate, br, zstd", + "accept-language": "en-US,en;q=0.9,ja;q=0.8,en-GB;q=0.7", + "cloudfront-forwarded-proto": "https", + "cloudfront-is-desktop-viewer": "true", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "cloudfront-is-tablet-viewer": "false", + "cloudfront-viewer-asn": "17676", + "cloudfront-viewer-country": "JP", + "content-length": "40", + "content-type": "application/json", + "host": "2ojpkjk2ejb57l7stgad5o4qiq.appsync-api.ap-northeast-1.amazonaws.com", + "origin": "https://ap-northeast-1.console.aws.amazon.com", + "priority": "u=1, i", + "referer": "https://ap-northeast-1.console.aws.amazon.com/", + "sec-ch-ua": "\"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"138\", \"Microsoft Edge\";v=\"138\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"Windows\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "cross-site", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0", + "via": "2.0 ee337d4db5c7ebfdc8ec0798a1ede776.cloudfront.net (CloudFront)", + "x-amz-cf-id": "O3ZflUCq6_TzxjouyYB3zg7-kl7Ze-gXbniM2jJ3hAOfDFpPMGRu3Q==", + "x-amz-user-agent": "AWS-Console-AppSync/", + "x-amzn-appsync-is-vpce-request": "false", + "x-amzn-remote-ip": "x.x.x.x", + "x-amzn-requestid": "7ada8740-bbf4-49e8-bf45-f10b3d67159b", + "x-amzn-trace-id": "Root=1-68713e21-7a03739120ad60703e794b22", + "x-api-key": "***", + "x-forwarded-for": "***", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + } + }, + "source": null, + "stash": {} +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json new file mode 100644 index 000000000..3862902ea --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-migrateuser-null-fields.json @@ -0,0 +1,31 @@ +{ + "version": "1", + "triggerSource": "UserMigration_Authentication", + "region": "", + "userPoolId": "", + "userName": "", + "callerContext": { + "awsSdkVersion": "", + "clientId": "" + }, + "request": { + "password": "", + "validationData": { + "exampleMetadataKey": "example metadata value" + }, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "userAttributes": null, + "finalUserStatus": "", + "messageAction": "", + "desiredDeliveryMediums": [ + "", + "" + ], + "forceAliasCreation": null + } +} + diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json new file mode 100644 index 000000000..be7fd4e2f --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-null-group-configuration.json @@ -0,0 +1,39 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "groupConfiguration": null, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsOverrideDetails": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email"], + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json new file mode 100644 index 000000000..d128a7ba1 --- /dev/null +++ b/lambda-events/src/fixtures/example-cognito-event-userpools-pretokengen-v2-null-group-configuration.json @@ -0,0 +1,50 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "region", + "userPoolId": "userPoolId", + "userName": "userName", + "callerContext": { + "awsSdkVersion": "calling aws sdk with version", + "clientId": "apps client id" + }, + "request": { + "userAttributes": { + "email": "email", + "phone_number": "phone_number" + }, + "scopes": ["scope-1", "scope-2"], + "groupConfiguration": null, + "clientMetadata": { + "exampleMetadataKey": "example metadata value" + } + }, + "response": { + "claimsAndScopeOverrideDetails": { + "idTokenGeneration": { + "claimsToAddOrOverride": { + "string": "string" + }, + "claimsToSuppress": ["string", "string"] + }, + "accessTokenGeneration": { + "claimsToAddOrOverride": { + "attribute_key2": "attribute_value2", + "attribute_key": "attribute_value" + }, + "claimsToSuppress": ["email", "phone"], + "scopesToAdd": ["scope-B", "scope-B"], + "scopesToSuppress": ["scope-C", "scope-D"] + }, + "groupOverrideDetails": { + "groupsToOverride": ["group-A", "group-B", "group-C"], + "iamRolesToOverride": [ + "arn:aws:iam::XXXXXXXXXXXX:role/sns_callerA", + "arn:aws:iam::XXXXXXXXX:role/sns_callerB", + "arn:aws:iam::XXXXXXXXXX:role/sns_callerC" + ], + "preferredRole": "arn:aws:iam::XXXXXXXXXXX:role/sns_caller" + } + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-create-managed-account.json b/lambda-events/src/fixtures/example-controltower-create-managed-account.json new file mode 100644 index 000000000..d42d5387b --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-create-managed-account.json @@ -0,0 +1,32 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "CreateManagedAccount", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "createManagedAccountStatus": { + "organizationalUnit": { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + }, + "account": { + "accountName": "TestAccount", + "accountId": "987654321098" + }, + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully created a managed account.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json new file mode 100644 index 000000000..c4c78c738 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-deregister-organizational-unit.json @@ -0,0 +1,28 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DeregisterOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "deregisterOrganizationalUnitStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully deregistered an organizational unit.", + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-disable-baseline.json b/lambda-events/src/fixtures/example-controltower-disable-baseline.json new file mode 100644 index 000000000..fcdba1096 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-disable-baseline.json @@ -0,0 +1,51 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DisableBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "disableBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "baselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-disable-guardrail.json b/lambda-events/src/fixtures/example-controltower-disable-guardrail.json new file mode 100644 index 000000000..bfd089131 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-disable-guardrail.json @@ -0,0 +1,36 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "DisableGuardrail", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "disableGuardrailStatus": { + "organizationalUnits": [ + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + } + ], + "guardrails": [ + { + "guardrailId": "AWS-GR_ENCRYPTED_VOLUMES", + "guardrailBehavior": "DETECTIVE" + } + ], + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully disabled a guardrail on an organizational unit.", + "requestTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-enable-baseline.json b/lambda-events/src/fixtures/example-controltower-enable-baseline.json new file mode 100644 index 000000000..999e69c7e --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-enable-baseline.json @@ -0,0 +1,48 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "EnableBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "enableBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [ + { + "key": "IdentityCenterEnabledBaselineArn", + "value": { + "untyped": { + "object": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/QRSTUVWXYZ123456" + } + } + } + ] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-enable-guardrail.json b/lambda-events/src/fixtures/example-controltower-enable-guardrail.json new file mode 100644 index 000000000..ce8e89c8a --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-enable-guardrail.json @@ -0,0 +1,36 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "EnableGuardrail", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "enableGuardrailStatus": { + "organizationalUnits": [ + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-12345678" + } + ], + "guardrails": [ + { + "guardrailId": "AWS-GR_ENCRYPTED_VOLUMES", + "guardrailBehavior": "DETECTIVE" + } + ], + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully enabled a guardrail on an organizational unit.", + "requestTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json new file mode 100644 index 000000000..803434acb --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-precheck-organizational-unit.json @@ -0,0 +1,41 @@ +{ + "eventVersion": "1.08", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "PrecheckOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "precheckOrganizationalUnitStatus": { + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333", + "failedPrechecks": [] + }, + "accounts": [ + { + "accountName": "ProdAccount1", + "accountId": "333333333333", + "failedPrechecks": [ + "CONFIG_RECORDER_CHECK" + ] + } + ], + "state": "FAILED", + "message": "AWS Control Tower failed the precheck for the organizational unit.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json b/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json new file mode 100644 index 000000000..7df2d25a6 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-register-organizational-unit.json @@ -0,0 +1,28 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "RegisterOrganizationalUnit", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "registerOrganizationalUnitStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully registered an organizational unit.", + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json b/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json new file mode 100644 index 000000000..eb6bd36d5 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-reset-enabled-baseline.json @@ -0,0 +1,39 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "ResetEnabledBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "resetEnabledBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "1.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json b/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json new file mode 100644 index 000000000..eab6e8191 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-setup-landing-zone.json @@ -0,0 +1,45 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "SetupLandingZone", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "setupLandingZoneStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully set up a new landing zone.", + "rootOrganizationalId": "r-abcd", + "organizationalUnits": [ + { + "organizationalUnitName": "Security", + "organizationalUnitId": "ou-abcd-11111111" + }, + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-22222222" + } + ], + "accounts": [ + { + "accountName": "Audit", + "accountId": "111111111111" + }, + { + "accountName": "Log archive", + "accountId": "222222222222" + } + ], + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json b/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json new file mode 100644 index 000000000..76bc40aad --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-enabled-baseline.json @@ -0,0 +1,48 @@ +{ + "eventVersion": "1.11", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateEnabledBaseline", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "requestParameters": null, + "responseElements": null, + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "managementEvent": true, + "recipientAccountId": "123456789012", + "serviceEventDetails": { + "updateEnabledBaselineStatus": { + "enabledBaselineDetails": { + "arn": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/ABCDEFGHIJKLMNOP", + "parentIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-12345678", + "targetIdentifier": "arn:aws:organizations::123456789012:ou/o-abcde12345/ou-abcd-33333333", + "baselineIdentifier": "arn:aws:controltower:us-east-1::baseline/ABCDE12345678", + "baselineVersion": "2.0", + "statusSummary": { + "lastOperationIdentifier": "aaaabbbb-cccc-dddd-eeee-ffffffffffff", + "status": "SUCCEEDED" + }, + "parameters": [ + { + "key": "IdentityCenterEnabledBaselineArn", + "value": { + "untyped": { + "object": "arn:aws:controltower:us-east-1:123456789012:enabledbaseline/QRSTUVWXYZ123456" + } + } + } + ] + }, + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + }, + "eventCategory": "Management" +} diff --git a/lambda-events/src/fixtures/example-controltower-update-landing-zone.json b/lambda-events/src/fixtures/example-controltower-update-landing-zone.json new file mode 100644 index 000000000..f99c3dc37 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-landing-zone.json @@ -0,0 +1,45 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateLandingZone", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "updateLandingZoneStatus": { + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully updated a landing zone.", + "rootOrganizationalId": "r-abcd", + "organizationalUnits": [ + { + "organizationalUnitName": "Security", + "organizationalUnitId": "ou-abcd-11111111" + }, + { + "organizationalUnitName": "Sandbox", + "organizationalUnitId": "ou-abcd-22222222" + } + ], + "accounts": [ + { + "accountName": "Audit", + "accountId": "111111111111" + }, + { + "accountName": "Log archive", + "accountId": "222222222222" + } + ], + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-controltower-update-managed-account.json b/lambda-events/src/fixtures/example-controltower-update-managed-account.json new file mode 100644 index 000000000..9a082f7b6 --- /dev/null +++ b/lambda-events/src/fixtures/example-controltower-update-managed-account.json @@ -0,0 +1,32 @@ +{ + "eventVersion": "1.05", + "userIdentity": { + "accountId": "123456789012", + "invokedBy": "AWS Internal" + }, + "eventTime": "2023-06-15T20:30:00Z", + "eventSource": "controltower.amazonaws.com", + "eventName": "UpdateManagedAccount", + "awsRegion": "us-east-1", + "sourceIPAddress": "AWS Internal", + "userAgent": "AWS Internal", + "eventID": "aaaabbbb-1111-2222-3333-444455556666", + "readOnly": false, + "eventType": "AwsServiceEvent", + "serviceEventDetails": { + "updateManagedAccountStatus": { + "organizationalUnit": { + "organizationalUnitName": "Production", + "organizationalUnitId": "ou-abcd-33333333" + }, + "account": { + "accountName": "TestAccount", + "accountId": "987654321098" + }, + "state": "SUCCEEDED", + "message": "AWS Control Tower successfully updated a managed account.", + "requestedTimestamp": "2023-06-15T20:00:00Z", + "completedTimestamp": "2023-06-15T20:30:00Z" + } + } +} diff --git a/lambda-events/src/fixtures/example-dynamodb-event-null-items.json b/lambda-events/src/fixtures/example-dynamodb-event-null-items.json new file mode 100644 index 000000000..ea24565e2 --- /dev/null +++ b/lambda-events/src/fixtures/example-dynamodb-event-null-items.json @@ -0,0 +1,131 @@ +{ + "Records": [ + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "userIdentity":{ + "type":"Service", + "principalId":"dynamodb.amazonaws.com" + }, + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": null, + "NewImage": null, + "OldImage": null, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + }, + { + "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "ApproximateCreationDateTime": 1480642020, + "Keys": { + "val": { + "S": "data" + }, + "key": { + "S": "binary" + } + }, + "NewImage": { + "val": { + "S": "data" + }, + "asdf1": { + "B": "AAEqQQ==" + }, + "b2": { + "B": "test" + }, + "asdf2": { + "BS": [ + "AAEqQQ==", + "QSoBAA==", + "AAEqQQ==" + ] + }, + "key": { + "S": "binary" + }, + "Binary": { + "B": "AAEqQQ==" + }, + "Boolean": { + "BOOL": true + }, + "BinarySet": { + "BS": [ + "AAEqQQ==", + "AAEqQQ==" + ] + }, + "List": { + "L": [ + { + "S": "Cookies" + }, + { + "S": "Coffee" + }, + { + "N": "3.14159" + } + ] + }, + "Map": { + "M": { + "Name": { + "S": "Joe" + }, + "Age": { + "N": "35" + } + } + }, + "FloatNumber": { + "N": "123.45" + }, + "IntegerNumber": { + "N": "123" + }, + "NumberSet": { + "NS": [ + "1234", + "567.8" + ] + }, + "Null": { + "NULL": true + }, + "String": { + "S": "Hello" + }, + "StringSet": { + "SS": [ + "Giraffe", + "Zebra" + ] + }, + "EmptyStringSet": { + "SS": [] + } + }, + "SequenceNumber": "1405400000000002063282832", + "SizeBytes": 54, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" + } + ] +} + diff --git a/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json b/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json new file mode 100644 index 000000000..cd31955fc --- /dev/null +++ b/lambda-events/src/fixtures/example-iot-custom-auth-request-x509.json @@ -0,0 +1,21 @@ +{ + "token": "aToken", + "signatureVerified": true, + "protocols": [ + "tls", + "mqtt" + ], + "protocolData": { + "tls": { + "serverName": "serverName", + "x509CertificatePem": "x509CertificatePem", + "principalId": "principalId" + }, + "mqtt": { + "clientId": "myClientId" + } + }, + "connectionMetadata": { + "id": "UUID" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-ses-bcc-event.json b/lambda-events/src/fixtures/example-ses-bcc-event.json new file mode 100644 index 000000000..79cf188e6 --- /dev/null +++ b/lambda-events/src/fixtures/example-ses-bcc-event.json @@ -0,0 +1,98 @@ +{ + "Records": [ + { + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "from": [ + "Amazon Web Services " + ], + "returnPath": "aws@amazon.com", + "messageId": "", + "date": "Mon, 5 Dec 2016 18:40:08 -0800", + "subject": "Test Subject" + }, + "source": "aws@amazon.com", + "timestamp": "1970-01-01T00:00:00.123Z", + "destination": [ + "lambda@amazon.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mx.amazon.com (mx.amazon.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id 6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1 for lambda@amazon.com; Tue, 06 Dec 2016 02:40:10 +0000 (UTC)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=iatn.net; s=amazon; h=mime-version:from:date:message-id:subject:to; bh=chlJxa/vZ11+0O9lf4tKDM/CcPjup2nhhdITm+hSf3c=; b=SsoNPK0wX7umtWnw8pln3YSib+E09XO99d704QdSc1TR1HxM0OTti/UaFxVD4e5b0+okBqo3rgVeWgNZ0sWZEUhBaZwSL3kTd/nHkcPexeV0XZqEgms1vmbg75F6vlz9igWflO3GbXyTRBNMM0gUXKU/686hpVW6aryEIfM/rLY=" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "From", + "value": "Amazon Web Services " + }, + { + "name": "Date", + "value": "Mon, 5 Dec 2016 18:40:08 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Test Subject" + }, + { + "name": "Bcc", + "value": "lambda@amazon.com" + }, + { + "name": "Content-Type", + "value": "multipart/alternative; boundary=94eb2c0742269658b10542f452a9" + } + ], + "headersTruncated": false, + "messageId": "6n4thuhcbhpfiuf25gshf70rss364fuejrvmqko1" + }, + "receipt": { + "recipients": [ + "lambda@amazon.com" + ], + "timestamp": "1970-01-01T00:00:00.123Z", + "spamVerdict": { + "status": "PASS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "dmarcPolicy": "reject", + "processingTimeMillis": 574, + "action": { + "type": "Lambda", + "invocationType": "Event", + "functionArn": "arn:aws:lambda:us-east-1:000000000000:function:my-ses-lambda-function" + }, + "spfVerdict": { + "status": "PASS" + }, + "virusVerdict": { + "status": "PASS" + } + } + }, + "eventSource": "aws:ses" + } + ] +} diff --git a/lambda-events/src/fixtures/example-sns-subscription-confirmation.json b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json new file mode 100644 index 000000000..563cf2b0d --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json @@ -0,0 +1,14 @@ +{ + "Type": "SubscriptionConfirmation", + "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + "Token": "2336412f37fb687f5d51e6e2425dacbbffff", + "Subject": null, + "MessageAttributes": {} +} diff --git a/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json new file mode 100644 index 000000000..880355696 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-unsubscribe-confirmation.json @@ -0,0 +1,13 @@ +{ + "Type": "UnsubscribeConfirmation", + "MessageId": "47138184-6831-46b8-8466-7168d3b90898", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "Message": "You have chosen to deactivate subscription arn:aws:sns:us-east-1:123456789012:MyTopic:00000000-0000-0000-0000-000000000000.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "Token": "2336412f37fb687f5d51e6e2425dacbbeeee", + "Subject": null, + "MessageAttributes": {} +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-response.json b/lambda-events/src/fixtures/example-vpc-lattice-response.json new file mode 100644 index 000000000..233996c06 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-response.json @@ -0,0 +1,10 @@ +{ + "statusCode": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req-123456789", + "cache-control": "no-cache" + }, + "body": "{\"orderId\": \"order-789\", \"status\": \"created\", \"message\": \"Order successfully created\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json new file mode 100644 index 000000000..bda2de8f9 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json @@ -0,0 +1,19 @@ +{ + "raw_path": "/api/product", + "method": "POST", + "headers": { + "accept": "*/*", + "user-agent": "curl/7.68.0", + "x-forwarded-for": "10.0.2.100", + "authorization": "Bearer abc123def456", + "multi": "abcd, DEF" + }, + "query_string_parameters": { + "category": "electronics", + "sort": "price", + "limit": "10", + "tags": "tv" + }, + "body": "{\"id\": 5, \"description\": \"TV\"}", + "is_base64_encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json new file mode 100644 index 000000000..8cf238b64 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json @@ -0,0 +1,29 @@ +{ + "version": "2.0", + "path": "/api/files/upload", + "method": "POST", + "headers": { + "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"], + "content-encoding": ["gzip"], + "content-length": ["1024"], + "x-forwarded-for": ["10.0.1.45"] + }, + "queryStringParameters": { + "uploadType": ["profile-image"] + }, + "body": "H4sIAAAAAAAAA+3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:user/john.developer", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875299234567" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json new file mode 100644 index 000000000..f4d1dbb40 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json @@ -0,0 +1,37 @@ +{ + "version": "2.0", + "path": "/api/external/data-sync", + "method": "POST", + "headers": { + "content-type": ["application/json"], + "user-agent": ["ExternalDataSync/2.1.0"], + "x-forwarded-for": ["203.45.67.89"], + "x-client-cert": ["present"], + "authorization": ["X509-Cert"] + }, + "queryStringParameters": { + "sync-type": ["incremental"], + "validate": ["true"] + }, + "body": "{\"timestamp\": \"2025-08-28T14:30:00Z\", \"records\": [{\"id\": \"ext-001\", \"data\": \"sample-data\"}], \"source\": \"external-partner-system\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/ExternalPartnerAccessRole", + "principalOrgID": "o-50dc6c495c0c9188", + "sessionName": "external-data-sync-session", + "x509IssuerOu": "Engineering Department", + "x509SanDns": "partner-system.external-company.com", + "x509SanNameCn": "Data Sync Service", + "x509SanUri": "https://partner-system.external-company.com/sync", + "x509SubjectCn": "external-partner-data-sync.external-company.com" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875199177430" + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json new file mode 100644 index 000000000..6901a37bd --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": null, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/mod.rs b/lambda-events/src/fixtures/mod.rs new file mode 100644 index 000000000..c059e05e7 --- /dev/null +++ b/lambda-events/src/fixtures/mod.rs @@ -0,0 +1,19 @@ +use std::fmt::Debug; + +use serde::{de::DeserializeOwned, Serialize}; + +/// verify that the given UTF8 JSON data deserializes to an event, +/// and that the serialized representation is an equivalent event. +/// +/// ## Returns +/// The deserialized event, for further verification if needed. +pub(crate) fn verify_serde_roundtrip(json_text: &[u8]) -> T +where + T: DeserializeOwned + Serialize + PartialEq + Debug, +{ + let parsed: T = serde_json::from_slice(json_text).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: T = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + parsed +} diff --git a/lambda-events/src/lib.rs b/lambda-events/src/lib.rs index e21cdc13a..541a4dc83 100644 --- a/lambda-events/src/lib.rs +++ b/lambda-events/src/lib.rs @@ -1,182 +1,242 @@ #![deny(rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "http")] +#[cfg_attr(docsrs, doc(cfg(feature = "http")))] pub use http; #[cfg(feature = "query_map")] +#[cfg_attr(docsrs, doc(cfg(feature = "query_map")))] pub use query_map; mod custom_serde; /// Encodings used in AWS Lambda json event values. pub mod encodings; #[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] pub mod time_window; /// AWS Lambda event definitions. pub mod event; +/// Reusable unit test utilities. +#[cfg(test)] +pub(crate) mod fixtures; + /// AWS Lambda event definitions for activemq. #[cfg(feature = "activemq")] +#[cfg_attr(docsrs, doc(cfg(feature = "activemq")))] pub use event::activemq; /// AWS Lambda event definitions for alb. #[cfg(feature = "alb")] +#[cfg_attr(docsrs, doc(cfg(feature = "alb")))] pub use event::alb; /// AWS Lambda event definitions for apigw. #[cfg(feature = "apigw")] +#[cfg_attr(docsrs, doc(cfg(feature = "apigw")))] pub use event::apigw; /// AWS Lambda event definitions for appsync. #[cfg(feature = "appsync")] +#[cfg_attr(docsrs, doc(cfg(feature = "appsync")))] pub use event::appsync; /// AWS Lambda event definitions for autoscaling. #[cfg(feature = "autoscaling")] +#[cfg_attr(docsrs, doc(cfg(feature = "autoscaling")))] pub use event::autoscaling; /// AWS Lambda event definitions for chime_bot. #[cfg(feature = "chime_bot")] +#[cfg_attr(docsrs, doc(cfg(feature = "chime_bot")))] pub use event::chime_bot; /// AWS Lambda event definitions for clientvpn. #[cfg(feature = "clientvpn")] +#[cfg_attr(docsrs, doc(cfg(feature = "clientvpn")))] pub use event::clientvpn; /// AWS Lambda event definitions for cloudformation #[cfg(feature = "cloudformation")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudformation")))] pub use event::cloudformation; /// AWS Lambda event definitions for CloudWatch alarms. #[cfg(feature = "cloudwatch_alarms")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_alarms")))] pub use event::cloudwatch_alarms; /// AWS Lambda event definitions for CloudWatch events. #[cfg(feature = "cloudwatch_events")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_events")))] pub use event::cloudwatch_events; /// AWS Lambda event definitions for cloudwatch_logs. #[cfg(feature = "cloudwatch_logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "cloudwatch_logs")))] pub use event::cloudwatch_logs; /// AWS Lambda event definitions for code_commit. #[cfg(feature = "code_commit")] +#[cfg_attr(docsrs, doc(cfg(feature = "code_commit")))] pub use event::code_commit; /// AWS Lambda event definitions for codebuild. #[cfg(feature = "codebuild")] +#[cfg_attr(docsrs, doc(cfg(feature = "codebuild")))] pub use event::codebuild; /// AWS Lambda event definitions for codedeploy. #[cfg(feature = "codedeploy")] +#[cfg_attr(docsrs, doc(cfg(feature = "codedeploy")))] pub use event::codedeploy; /// AWS Lambda event definitions for codepipeline_cloudwatch. #[cfg(feature = "codepipeline_cloudwatch")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_cloudwatch")))] pub use event::codepipeline_cloudwatch; /// AWS Lambda event definitions for codepipeline_job. #[cfg(feature = "codepipeline_job")] +#[cfg_attr(docsrs, doc(cfg(feature = "codepipeline_job")))] pub use event::codepipeline_job; /// AWS Lambda event definitions for cognito. #[cfg(feature = "cognito")] +#[cfg_attr(docsrs, doc(cfg(feature = "cognito")))] pub use event::cognito; /// AWS Lambda event definitions for config. #[cfg(feature = "config")] +#[cfg_attr(docsrs, doc(cfg(feature = "config")))] pub use event::config; /// AWS Lambda event definitions for connect. #[cfg(feature = "connect")] +#[cfg_attr(docsrs, doc(cfg(feature = "connect")))] pub use event::connect; +/// AWS Lambda event definitions for AWS Control Tower lifecycle events. +#[cfg(feature = "controltower")] +#[cfg_attr(docsrs, doc(cfg(feature = "controltower")))] +pub use event::controltower; + /// AWS Lambda event definitions for dynamodb. #[cfg(feature = "dynamodb")] +#[cfg_attr(docsrs, doc(cfg(feature = "dynamodb")))] pub use event::dynamodb; /// AWS Lambda event definitions for ecr_scan. #[cfg(feature = "ecr_scan")] +#[cfg_attr(docsrs, doc(cfg(feature = "ecr_scan")))] pub use event::ecr_scan; /// AWS Lambda event definitions for firehose. #[cfg(feature = "firehose")] +#[cfg_attr(docsrs, doc(cfg(feature = "firehose")))] pub use event::firehose; /// AWS Lambda event definitions for iam. #[cfg(feature = "iam")] +#[cfg_attr(docsrs, doc(cfg(feature = "iam")))] pub use event::iam; /// AWS Lambda event definitions for iot. #[cfg(feature = "iot")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot")))] pub use event::iot; /// AWS Lambda event definitions for iot_1_click. #[cfg(feature = "iot_1_click")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_1_click")))] pub use event::iot_1_click; /// AWS Lambda event definitions for iot_button. #[cfg(feature = "iot_button")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_button")))] pub use event::iot_button; /// AWS Lambda event definitions for iot_deprecated. #[cfg(feature = "iot_deprecated")] +#[cfg_attr(docsrs, doc(cfg(feature = "iot_deprecated")))] pub use event::iot_deprecated; /// AWS Lambda event definitions for kafka. #[cfg(feature = "kafka")] +#[cfg_attr(docsrs, doc(cfg(feature = "kafka")))] pub use event::kafka; /// AWS Lambda event definitions for kinesis. #[cfg(feature = "kinesis")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis")))] pub use event::kinesis; /// AWS Lambda event definitions for kinesis_analytics. #[cfg(feature = "kinesis_analytics")] +#[cfg_attr(docsrs, doc(cfg(feature = "kinesis_analytics")))] pub use event::kinesis::analytics as kinesis_analytics; /// AWS Lambda event definitions for lambda_function_urls. #[cfg(feature = "lambda_function_urls")] +#[cfg_attr(docsrs, doc(cfg(feature = "lambda_function_urls")))] pub use event::lambda_function_urls; /// AWS Lambda event definitions for lex. #[cfg(feature = "lex")] +#[cfg_attr(docsrs, doc(cfg(feature = "lex")))] pub use event::lex; /// AWS Lambda event definitions for rabbitmq. #[cfg(feature = "rabbitmq")] +#[cfg_attr(docsrs, doc(cfg(feature = "rabbitmq")))] pub use event::rabbitmq; /// AWS Lambda event definitions for s3. #[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] pub use event::s3; /// AWS Lambda event definitions for s3_batch_job. #[cfg(feature = "s3")] +#[cfg_attr(docsrs, doc(cfg(feature = "s3")))] pub use event::s3::batch_job as s3_batch_job; /// AWS Lambda event definitions for secretsmanager. #[cfg(feature = "secretsmanager")] +#[cfg_attr(docsrs, doc(cfg(feature = "secretsmanager")))] pub use event::secretsmanager; /// AWS Lambda event definitions for ses. #[cfg(feature = "ses")] +#[cfg_attr(docsrs, doc(cfg(feature = "ses")))] pub use event::ses; /// AWS Lambda event definitions for SNS. #[cfg(feature = "sns")] +#[cfg_attr(docsrs, doc(cfg(feature = "sns")))] pub use event::sns; /// AWS Lambda event definitions for SQS. #[cfg(feature = "sqs")] +#[cfg_attr(docsrs, doc(cfg(feature = "sqs")))] pub use event::sqs; /// AWS Lambda event definitions for streams. #[cfg(feature = "streams")] +#[cfg_attr(docsrs, doc(cfg(feature = "streams")))] pub use event::streams; /// AWS Lambda event definitions for documentdb. #[cfg(feature = "documentdb")] +#[cfg_attr(docsrs, doc(cfg(feature = "documentdb")))] pub use event::documentdb; /// AWS Lambda event definitions for EventBridge. #[cfg(feature = "eventbridge")] +#[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub use event::eventbridge; + +/// AWS Lambda event definitions for VPC lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub use event::vpc_lattice; diff --git a/lambda-events/src/time_window.rs b/lambda-events/src/time_window.rs index edc9beb5f..35c038979 100644 --- a/lambda-events/src/time_window.rs +++ b/lambda-events/src/time_window.rs @@ -2,11 +2,12 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::custom_serde::deserialize_lambda_map; +use crate::custom_serde::deserialize_nullish; /// `Window` is the object that captures the time window for the records in the event when using the tumbling windows feature -/// Kinesis: https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows -/// DDB: https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-windows +/// Kinesis: +/// DDB: +#[non_exhaustive] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct Window { @@ -24,15 +25,16 @@ impl Default for Window { } /// `TimeWindowProperties` is the object that captures properties that relate to the tumbling windows feature -/// Kinesis: https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows -/// DDB: https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-windows +/// Kinesis: +/// DDB: +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimeWindowProperties { /// Time window for the records in the event. pub window: Window, /// State being built up to this invoke in the time window. - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub state: HashMap, /// Shard id of the records @@ -51,13 +53,14 @@ pub struct TimeWindowProperties { } /// `TimeWindowEventResponseProperties` is the object that captures response properties that relate to the tumbling windows feature -/// Kinesis: https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows -/// DDB: https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-windows +/// Kinesis: +/// DDB: +#[non_exhaustive] #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct TimeWindowEventResponseProperties { /// State being built up to this invoke in the time window. - #[serde(deserialize_with = "deserialize_lambda_map")] + #[serde(deserialize_with = "deserialize_nullish")] #[serde(default)] pub state: HashMap, } diff --git a/lambda-extension/CHANGELOG.md b/lambda-extension/CHANGELOG.md new file mode 100644 index 000000000..66621a59e --- /dev/null +++ b/lambda-extension/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.1.0...lambda-extension-v1.1.1) - 2026-09-02 + +### Other + +- updated the following local packages: lambda_runtime_api_client + +## [1.1.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.4...lambda-extension-v1.1.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.0.4](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.3...lambda-extension-v1.0.4) - 2026-03-19 + +### Other + +- updated the following local packages: lambda_runtime_api_client + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda-extension-v1.0.2...lambda-extension-v1.0.3) - 2026-03-12 + +### Added + +- Make Telemetry API log record type generic ([#1098](https://github.com/aws/aws-lambda-rust-runtime/pull/1098)) diff --git a/lambda-extension/Cargo.toml b/lambda-extension/Cargo.toml index 16b6dace9..0aefbd84d 100644 --- a/lambda-extension/Cargo.toml +++ b/lambda-extension/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "lambda-extension" -version = "0.11.0" +version = "1.1.1" edition = "2021" +rust-version = "1.84.0" authors = [ "David Calavera ", "Harold Sun ", ] description = "AWS Lambda Extension API" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "README.md" @@ -19,13 +20,12 @@ tracing = ["lambda_runtime_api_client/tracing"] [dependencies] async-stream = "0.3" -bytes = { workspace = true } chrono = { workspace = true, features = ["serde"] } http = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true, features = ["http1", "client", "server"] } hyper-util = { workspace = true } -lambda_runtime_api_client = { version = "0.11", path = "../lambda-runtime-api-client" } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client" } serde = { version = "1", features = ["derive"] } serde_json = "^1" tokio = { version = "1.0", features = [ @@ -37,3 +37,6 @@ tokio = { version = "1.0", features = [ tokio-stream = "0.1.2" tower = { workspace = true, features = ["make", "util"] } tracing = { version = "0.1", features = ["log"] } + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-extension/src/extension.rs b/lambda-extension/src/extension.rs index 15e0befd3..16cfd1f66 100644 --- a/lambda-extension/src/extension.rs +++ b/lambda-extension/src/extension.rs @@ -3,12 +3,13 @@ use http_body_util::BodyExt; use hyper::{body::Incoming, server::conn::http1, service::service_fn}; use hyper_util::rt::tokio::TokioIo; -use lambda_runtime_api_client::Client; -use serde::Deserialize; +use lambda_runtime_api_client::PooledClient as Client; +use serde::{de::DeserializeOwned, Deserialize}; use std::{ convert::Infallible, fmt, future::{ready, Future}, + marker::PhantomData, net::SocketAddr, path::PathBuf, pin::Pin, @@ -29,7 +30,7 @@ const DEFAULT_LOG_PORT_NUMBER: u16 = 9002; const DEFAULT_TELEMETRY_PORT_NUMBER: u16 = 9003; /// An Extension that runs event, log and telemetry processors -pub struct Extension<'a, E, L, T> { +pub struct Extension<'a, E, L, T, TL = String> { extension_name: Option<&'a str>, events: Option<&'a [&'a str]>, events_processor: E, @@ -41,6 +42,7 @@ pub struct Extension<'a, E, L, T> { telemetry_processor: Option, telemetry_buffering: Option, telemetry_port_number: u16, + _telemetry_record_type: PhantomData, } impl Extension<'_, Identity, MakeIdentity>, MakeIdentity>> { @@ -58,6 +60,7 @@ impl Extension<'_, Identity, MakeIdentity>, MakeIden telemetry_buffering: None, telemetry_processor: None, telemetry_port_number: DEFAULT_TELEMETRY_PORT_NUMBER, + _telemetry_record_type: PhantomData, } } } @@ -70,7 +73,7 @@ impl Default } } -impl<'a, E, L, T> Extension<'a, E, L, T> +impl<'a, E, L, T, TL> Extension<'a, E, L, T, TL> where E: Service, E::Future: Future>, @@ -85,12 +88,13 @@ where L::Future: Send, // Fixme: 'static bound might be too restrictive - T: MakeService<(), Vec, Response = ()> + Send + Sync + 'static, - T::Service: Service, Response = ()> + Send + Sync, - >>::Future: Send + 'a, + T: MakeService<(), Vec>, Response = ()> + Send + Sync + 'static, + T::Service: Service>, Response = ()> + Send + Sync, + >>>::Future: Send + 'a, T::Error: Into + fmt::Debug, T::MakeError: Into + fmt::Debug, T::Future: Send, + TL: DeserializeOwned + Send + 'static, { /// Create a new [`Extension`] with a given extension name pub fn with_extension_name(self, extension_name: &'a str) -> Self { @@ -110,7 +114,7 @@ where } /// Create a new [`Extension`] with a service that receives Lambda events. - pub fn with_events_processor(self, ep: N) -> Extension<'a, N, L, T> + pub fn with_events_processor(self, ep: N) -> Extension<'a, N, L, T, TL> where N: Service, N::Future: Future>, @@ -128,11 +132,12 @@ where telemetry_buffering: self.telemetry_buffering, telemetry_processor: self.telemetry_processor, telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, } } /// Create a new [`Extension`] with a service that receives Lambda logs. - pub fn with_logs_processor(self, lp: N) -> Extension<'a, E, N, T> + pub fn with_logs_processor(self, lp: N) -> Extension<'a, E, N, T, TL> where N: Service<()>, N::Future: Future>, @@ -150,6 +155,7 @@ where telemetry_buffering: self.telemetry_buffering, telemetry_processor: self.telemetry_processor, telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, } } @@ -179,7 +185,11 @@ where } /// Create a new [`Extension`] with a service that receives Lambda telemetry data. - pub fn with_telemetry_processor(self, lp: N) -> Extension<'a, E, L, N> + /// + /// By default, telemetry log records are deserialized as `String`, but + /// it's possible to configure Lambda functions to emit logs in JSON format. + /// For more information, refer to [`Self::with_telemetry_record_type`]. + pub fn with_telemetry_processor(self, lp: N) -> Extension<'a, E, L, N, TL> where N: Service<()>, N::Future: Future>, @@ -197,6 +207,7 @@ where telemetry_types: self.telemetry_types, telemetry_buffering: self.telemetry_buffering, telemetry_port_number: self.telemetry_port_number, + _telemetry_record_type: self._telemetry_record_type, } } @@ -233,7 +244,7 @@ where /// extension, it is safe to call `lambda_runtime::run` once the future returned by this /// function resolves. pub async fn register(self) -> Result, Error> { - let client = &Client::builder().build()?; + let client = &Client::builder().build(); let register_res = register(client, self.extension_name, self.events).await?; @@ -259,7 +270,7 @@ where let io = TokioIo::new(tcp); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, make_service).await { - println!("Error serving connection: {:?}", err); + println!("Error serving connection: {err:?}"); } }); } @@ -305,7 +316,7 @@ where let io = TokioIo::new(tcp); tokio::task::spawn(async move { if let Err(err) = http1::Builder::new().serve_connection(io, make_service).await { - println!("Error serving connection: {:?}", err); + println!("Error serving connection: {err:?}"); } }); } @@ -345,6 +356,48 @@ where } } +impl<'a, E, L> Extension<'a, E, L, MakeIdentity>> { + /// Set the deserialization type for telemetry log records. + /// + /// By default, telemetry log records are deserialized as `String`, but + /// it's possible to configure Lambda functions to emit logs in JSON format. + /// Use this method to deserialize into a different type, such as + /// `serde_json::Value`. + /// + /// Must be called before [`Self::with_telemetry_processor`]. + /// + /// ``` + /// use lambda_extension::{Extension, LambdaTelemetry, SharedService, service_fn}; + /// + /// async fn handler(events: Vec>) -> Result<(), lambda_extension::Error> { + /// for event in &events { + /// println!("{event:?}"); + /// } + /// Ok(()) + /// } + /// + /// let _ext = Extension::new() + /// .with_telemetry_record_type::() + /// .with_telemetry_processor(SharedService::new(service_fn(handler))); + /// ``` + pub fn with_telemetry_record_type(self) -> Extension<'a, E, L, MakeIdentity>>, N> { + Extension { + _telemetry_record_type: PhantomData, + telemetry_processor: None, + events_processor: self.events_processor, + extension_name: self.extension_name, + events: self.events, + log_types: self.log_types, + log_buffering: self.log_buffering, + logs_processor: self.logs_processor, + log_port_number: self.log_port_number, + telemetry_types: self.telemetry_types, + telemetry_buffering: self.telemetry_buffering, + telemetry_port_number: self.telemetry_port_number, + } + } +} + /// An extension registered by calling [`Extension::register`]. pub struct RegisteredExtension { /// The ID of the registered extension. This ID is unique per extension and remains constant @@ -375,7 +428,7 @@ where /// [shutdown](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle-shutdown) /// Lambda lifecycle phases. pub async fn run(self) -> Result<(), Error> { - let client = &Client::builder().build()?; + let client = &Client::builder().build(); let mut ep = self.events_processor; let extension_id = &self.extension_id; diff --git a/lambda-extension/src/lib.rs b/lambda-extension/src/lib.rs index 81c16337f..b6aec18fd 100644 --- a/lambda-extension/src/lib.rs +++ b/lambda-extension/src/lib.rs @@ -1,6 +1,7 @@ #![deny(clippy::all, clippy::cargo)] #![allow(clippy::multiple_crate_versions, clippy::type_complexity)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! This module includes utilities to create Lambda Runtime Extensions. //! @@ -25,6 +26,7 @@ pub mod requests; /// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. #[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] pub use lambda_runtime_api_client::tracing; /// Execute the given events processor diff --git a/lambda-extension/src/logs.rs b/lambda-extension/src/logs.rs index c3b0cda28..541dedc2d 100644 --- a/lambda-extension/src/logs.rs +++ b/lambda-extension/src/logs.rs @@ -12,7 +12,7 @@ use tracing::{error, trace}; use crate::{Error, ExtensionError}; /// Payload received from the Lambda Logs API -/// See: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-msg +/// See: #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct LambdaLog { /// Time when the log was generated diff --git a/lambda-extension/src/telemetry.rs b/lambda-extension/src/telemetry.rs index a77608925..6227001f9 100644 --- a/lambda-extension/src/telemetry.rs +++ b/lambda-extension/src/telemetry.rs @@ -3,7 +3,7 @@ use http::{Request, Response}; use http_body_util::BodyExt; use hyper::body::Incoming; use lambda_runtime_api_client::body::Body; -use serde::{Deserialize, Serialize}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{boxed::Box, fmt, sync::Arc}; use tokio::sync::Mutex; use tower::Service; @@ -11,23 +11,23 @@ use tracing::{error, trace}; /// Payload received from the Telemetry API #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -pub struct LambdaTelemetry { +pub struct LambdaTelemetry { /// Time when the telemetry was generated pub time: DateTime, /// Telemetry record entry #[serde(flatten)] - pub record: LambdaTelemetryRecord, + pub record: LambdaTelemetryRecord, } /// Record in a LambdaTelemetry entry #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(tag = "type", content = "record", rename_all = "lowercase")] -pub enum LambdaTelemetryRecord { +pub enum LambdaTelemetryRecord { /// Function log records - Function(String), + Function(L), /// Extension log records - Extension(String), + Extension(L), /// Platform init start record #[serde(rename = "platform.initStart", rename_all = "camelCase")] @@ -269,14 +269,15 @@ pub struct RuntimeDoneMetrics { /// /// This takes an `hyper::Request` and transforms it into `Vec` for the /// underlying `Service` to process. -pub(crate) async fn telemetry_wrapper( +pub(crate) async fn telemetry_wrapper( service: Arc>, req: Request, ) -> Result, Box> where - S: Service, Response = ()>, + S: Service>, Response = ()>, S::Error: Into> + fmt::Debug, S::Future: Send, + L: DeserializeOwned, { trace!("Received telemetry request"); // Parse the request body as a Vec @@ -291,7 +292,7 @@ where } }; - let telemetry: Vec = match serde_json::from_slice(&body.to_bytes()) { + let telemetry: Vec> = match serde_json::from_slice(&body.to_bytes()) { Ok(telemetry) => telemetry, Err(e) => { error!("Error parsing telemetry: {}", e); @@ -319,17 +320,17 @@ mod deserialization_tests { use chrono::{TimeDelta, TimeZone}; macro_rules! deserialize_tests { - ($($name:ident: $value:expr,)*) => { + ($($name:ident$(<$log:ty>)?: $value:expr,)*) => { $( #[test] fn $name() { let (input, expected) = $value; - let actual = serde_json::from_str::(&input).expect("unable to deserialize"); + let actual = serde_json::from_str::)?>(&input).expect("unable to deserialize"); assert!(actual.record == expected); } )* - } + }; } deserialize_tests! { @@ -339,12 +340,24 @@ mod deserialization_tests { LambdaTelemetryRecord::Function("hello world".to_string()), ), + // function (json) + function_generic: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "function", "record": true}"#, + LambdaTelemetryRecord::Function(true), + ), + // extension extension: ( r#"{"time": "2020-08-20T12:31:32.123Z","type": "extension", "record": "hello world"}"#, LambdaTelemetryRecord::Extension("hello world".to_string()), ), + // extension (json) + extension_generic: ( + r#"{"time": "2020-08-20T12:31:32.123Z","type": "extension", "record": true}"#, + LambdaTelemetryRecord::Extension(true), + ), + // platform.start platform_start: ( r#"{"time":"2022-10-21T14:05:03.165Z","type":"platform.start","record":{"requestId":"459921b5-681c-4a96-beb0-81e0aa586026","version":"$LATEST","tracing":{"spanId":"24cd7d670fa455f0","type":"X-Amzn-Trace-Id","value":"Root=1-6352a70e-1e2c502e358361800241fd45;Parent=35465b3a9e2f7c6a;Sampled=1"}}}"#, @@ -477,11 +490,11 @@ mod serialization_tests { use super::*; macro_rules! serialize_tests { - ($($name:ident: $value:expr,)*) => { + ($($name:ident$(<$log:ty>)?: $value:expr,)*) => { $( #[test] fn $name() { - let (input, expected) = $value; + let (input, expected): (LambdaTelemetry$(<$log>)?, &str) = $value; let actual = serde_json::to_string(&input).expect("unable to serialize"); println!("Input: {:?}\n", input); println!("Expected:\n {:?}\n", expected); @@ -490,7 +503,7 @@ mod serialization_tests { assert!(actual == expected); } )* - } + }; } serialize_tests! { @@ -502,6 +515,14 @@ mod serialization_tests { }, r#"{"time":"2023-11-28T12:00:09Z","type":"function","record":"hello world"}"#, ), + // function (json) + function_generic: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Function(true), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"function","record":true}"#, + ), // extension extension: ( LambdaTelemetry { @@ -510,6 +531,14 @@ mod serialization_tests { }, r#"{"time":"2023-11-28T12:00:09Z","type":"extension","record":"hello world"}"#, ), + // extension (json) + extension_generic: ( + LambdaTelemetry { + time: Utc.with_ymd_and_hms(2023, 11, 28, 12, 0, 9).unwrap(), + record: LambdaTelemetryRecord::Extension(true), + }, + r#"{"time":"2023-11-28T12:00:09Z","type":"extension","record":true}"#, + ), //platform.Start platform_start: ( LambdaTelemetry{ diff --git a/lambda-http/CHANGELOG.md b/lambda-http/CHANGELOG.md new file mode 100644 index 000000000..e7965a4a2 --- /dev/null +++ b/lambda-http/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.3.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.3.1...lambda_http-v1.3.2) - 2026-09-29 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.3.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.3.0...lambda_http-v1.3.1) - 2026-09-02 + +### Other + +- updated the following local packages: lambda_runtime_api_client, lambda_runtime + +## [1.3.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.2.1...lambda_http-v1.3.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.2.0...lambda_http-v1.2.1) - 2026-05-25 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.3...lambda_http-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +### Fixed + +- *(lambda-http)* into_api_gateway_v2_request joins cookies with "; " ([#1143](https://github.com/aws/aws-lambda-rust-runtime/pull/1143)) + +### Other + +- Added `builders` pass-through feature to enable `aws_lambda_events/builders` ([#1146](https://github.com/aws/aws-lambda-rust-runtime/pull/1146)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.2...lambda_http-v1.1.3) - 2026-04-16 + +### Other + +- updated the following local packages: lambda_runtime + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_http-v1.1.1...lambda_http-v1.1.2) - 2026-03-19 + +### Other + +- updated the following local packages: lambda_runtime_api_client, lambda_runtime + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_http-v1.1.1) - 2026-03-11 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag with `run_concurrent()` and `BoxCloneService` for concurrent streaming responses ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) + +### Other + +- *(lambda-managed-instances)* add `tokio_unstable` to known cfgs to avoid linter warns ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* warn on `run()` with `AWS_LAMBDA_MAX_CONCURRENCY`, rename feature `experimental-concurrency` to `concurrency-tokio` ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* verify request-ID isolation in concurrent exec ([#1086](https://github.com/aws/aws-lambda-rust-runtime/pull/1086)) +- Introducing Harness Testing ([#1103](https://github.com/aws/aws-lambda-rust-runtime/pull/1103)) +- disable default features of lambda-runtime ([#1093](https://github.com/aws/aws-lambda-rust-runtime/pull/1093)) +- *(deps)* update axum-extra requirement from 0.10.2 to 0.12.5 ([#1079](https://github.com/aws/aws-lambda-rust-runtime/pull/1079)) + +## [1.1.0-rc1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_http-v1.1.0-rc1) - 2026-02-04 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag with `run_concurrent()` and `BoxCloneService` for concurrent streaming responses ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index 46198dca5..245c126b0 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -1,44 +1,47 @@ [package] name = "lambda_http" -version = "0.14.0" +version = "1.3.2" authors = [ "David Calavera ", "Harold Sun ", ] edition = "2021" +rust-version = "1.84.0" description = "Application Load Balancer and API Gateway event types for AWS Lambda" keywords = ["AWS", "Lambda", "APIGateway", "ALB", "API"] license = "Apache-2.0" -homepage = "https://github.com/awslabs/aws-lambda-rust-runtime" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +homepage = "https://github.com/aws/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" documentation = "https://docs.rs/lambda_runtime" categories = ["web-programming::http-server"] readme = "README.md" [features] -default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb", "tracing"] +default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb", "vpc_lattice", "tracing"] apigw_rest = [] apigw_http = [] apigw_websockets = [] alb = [] +vpc_lattice = [] pass_through = [] +builders = ["aws_lambda_events/builders"] +catch-all-fields = ["aws_lambda_events/catch-all-fields"] tracing = ["lambda_runtime/tracing"] # enables access to the Tracing utilities opentelemetry = ["lambda_runtime/opentelemetry"] # enables access to the OpenTelemetry layers and utilities anyhow = ["lambda_runtime/anyhow"] # enables From for Diagnostic for anyhow error types, see README.md for more info eyre = ["lambda_runtime/eyre"] # enables From for Diagnostic for eyre error types, see README.md for more info miette = ["lambda_runtime/miette"] # enables From for Diagnostic for miette error types, see README.md for more info +concurrency-tokio = ["lambda_runtime/concurrency-tokio"] [dependencies] -base64 = { workspace = true } bytes = { workspace = true } encoding_rs = "0.8" -futures = { workspace = true } futures-util = { workspace = true } http = { workspace = true } http-body = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } -lambda_runtime = { version = "0.13.0", path = "../lambda-runtime" } +lambda_runtime = { version = "1.5.0", path = "../lambda-runtime", default-features = false} mime = "0.3" percent-encoding = "2.2" pin-project-lite = { workspace = true } @@ -50,14 +53,19 @@ url = "2.2" [dependencies.aws_lambda_events] path = "../lambda-events" -version = "0.16.0" +version = "1.2" default-features = false -features = ["alb", "apigw"] +features = ["alb", "apigw", "vpc_lattice"] [dev-dependencies] -axum-core = "0.4.3" -axum-extra = { version = "0.9.2", features = ["query"] } -lambda_runtime_api_client = { version = "0.11.1", path = "../lambda-runtime-api-client" } +axum-core = "0.5.4" + +axum-extra = { version = "0.12.5", features = ["query"] } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client" } + log = "^0.4" maplit = "1.0" tokio = { version = "1.0", features = ["macros"] } + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-http/README.md b/lambda-http/README.md index 6b394964c..b31a8c8de 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -2,7 +2,9 @@ [![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) -**`lambda-http`** is an abstraction that takes payloads from different services and turns them into http objects, making it easy to write API Gateway proxy event focused Lambda functions in Rust. +**`lambda-http`** is an abstraction that takes HTTP-shaped payloads from different AWS services and turns them +into standard `http` objects, making it easy to write Lambda functions in Rust that serve HTTP traffic +regardless of the upstream trigger. lambda-http handler is made of: @@ -14,17 +16,18 @@ We are able to handle requests from: * [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST, HTTP and WebSockets API lambda integrations * AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) * AWS [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) +* AWS [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) -Thanks to the `Request` type we can seamlessly handle proxy integrations without the worry to specify the specific service type. +Thanks to the `Request` type we can seamlessly handle all of these triggers without having to write service-specific code. -There is also an extension for `lambda_http::Request` structs that provides access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html) features. +There is also an extension for `lambda_http::Request` structs that provides access to [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format), [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html), and [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) features. For example some handy extensions: * `query_string_parameters` - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request * `path_parameters` - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{qux}` associated with the request * `lambda_context` - Return the Lambda context for the invocation; see the [runtime docs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-next) -* `request_context` - Return the ALB/API Gateway request context +* `request_context` - Return the ALB, API Gateway, or VPC Lattice request context * payload - Return the Result of a payload parsed into a type that implements `serde::Deserialize` See the `lambda_http::RequestPayloadExt` and `lambda_http::RequestExt` traits for more extensions. @@ -169,7 +172,7 @@ pub fn custom_authorizer_response(effect: &str, principal: &str, method_arn: &st ApiGatewayCustomAuthorizerResponse { principal_id: Some(principal.to_owned()), policy_document: policy, - context: json!({ "email": principal }), // https://github.com/awslabs/aws-lambda-rust-runtime/discussions/548 + context: json!({ "email": principal }), // https://github.com/aws/aws-lambda-rust-runtime/discussions/548 usage_identifier_key: None, } } @@ -238,16 +241,19 @@ If you don't want to receive the stage as part of the path, you can set the envi ## Feature flags -`lambda_http` is a wrapper for HTTP events coming from three different services, Amazon Load Balancer (ALB), Amazon Api Gateway (APIGW), and AWS Lambda Function URLs. Amazon Api Gateway can also send events from three different endpoints, REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions. +`lambda_http` is a wrapper for HTTP events coming from four different AWS services: Application Load Balancer (ALB), API Gateway, Lambda Function URLs, and VPC Lattice. API Gateway can send events from three different endpoint types: REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions. By default, `lambda_http` compiles your function to support any of those services. This increases the compile time of your function because we have to generate code for all the sources. In reality, you'll usually put a Lambda function only behind one of those sources. You can choose which source to generate code for with feature flags. -The available features flags for `lambda_http` are the following: +The available feature flags for `lambda_http` are the following: - `alb`: for events coming from [Amazon Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). -- `apigw_rest`: for events coming from [Amazon API Gateway Rest APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). +- `apigw_rest`: for events coming from [Amazon API Gateway REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). - `apigw_http`: for events coming from [Amazon API Gateway HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html). - `apigw_websockets`: for events coming from [Amazon API Gateway WebSockets](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html). +- `vpc_lattice`: for events coming from [AWS VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html). +- `catch-all-fields`: enables catch-all fields in event types to preserve unknown JSON fields during deserialization. +- `builders`: enables builder pattern support for request/response types in `aws_lambda_events`, useful for constructing test fixtures. If you only want to support one of these sources, you can disable the default features, and enable only the source that you care about in your package's `Cargo.toml` file. Substitute the dependency line for `lambda_http` for the snippet below, changing the feature that you want to enable: diff --git a/lambda-http/src/deserializer.rs b/lambda-http/src/deserializer.rs index 4c0ad519c..776151995 100644 --- a/lambda-http/src/deserializer.rs +++ b/lambda-http/src/deserializer.rs @@ -7,6 +7,9 @@ use aws_lambda_events::apigw::ApiGatewayProxyRequest; use aws_lambda_events::apigw::ApiGatewayV2httpRequest; #[cfg(feature = "apigw_websockets")] use aws_lambda_events::apigw::ApiGatewayWebsocketProxyRequest; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::VpcLatticeRequestV2; + use serde::{de::Error, Deserialize}; use serde_json::value::RawValue; @@ -20,7 +23,7 @@ impl<'de> Deserialize<'de> for LambdaRequest { where D: serde::Deserializer<'de>, { - let raw_value: Box = Box::deserialize(deserializer)?; + let raw_value: Box = Box::::deserialize(deserializer)?; let data = raw_value.get(); #[cfg(feature = "apigw_rest")] @@ -39,6 +42,10 @@ impl<'de> Deserialize<'de> for LambdaRequest { if let Ok(res) = serde_json::from_str::(data) { return Ok(LambdaRequest::WebSocket(res)); } + #[cfg(feature = "vpc_lattice")] + if let Ok(res) = serde_json::from_str::(data) { + return Ok(LambdaRequest::VpcLatticeV2(res)); + } #[cfg(feature = "pass_through")] if PASS_THROUGH_ENABLED { return Ok(LambdaRequest::PassThrough(data.to_string())); @@ -61,7 +68,7 @@ mod tests { LambdaRequest::ApiGatewayV1(req) => { assert_eq!("12345678912", req.request_context.account_id.unwrap()); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), } } @@ -74,7 +81,7 @@ mod tests { LambdaRequest::ApiGatewayV2(req) => { assert_eq!("123456789012", req.request_context.account_id.unwrap()); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), } } @@ -87,7 +94,7 @@ mod tests { LambdaRequest::ApiGatewayV1(req) => { assert_eq!("123456789012", req.request_context.account_id.unwrap()); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), } } @@ -100,7 +107,7 @@ mod tests { LambdaRequest::ApiGatewayV2(req) => { assert_eq!("123456789012", req.request_context.account_id.unwrap()); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), } } @@ -118,7 +125,7 @@ mod tests { req.request_context.elb.target_group_arn.unwrap() ); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), } } @@ -132,7 +139,23 @@ mod tests { LambdaRequest::WebSocket(req) => { assert_eq!("CONNECT", req.request_context.event_type.unwrap()); } - other => panic!("unexpected request variant: {:?}", other), + other => panic!("unexpected request variant: {other:?}"), + } + } + + #[test] + fn test_deserialize_vpc_lattice() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-vpc-lattice-v2-request.json"); + + let req: LambdaRequest = serde_json::from_slice(data).expect("failed to deserialize vpc lattice data"); + match req { + LambdaRequest::VpcLatticeV2(req) => { + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + req.request_context.target_group_arn + ); + } + other => panic!("unexpected request variant: {other:?}"), } } diff --git a/lambda-http/src/ext/extensions.rs b/lambda-http/src/ext/extensions.rs index cfbdaec29..65bf8ac07 100644 --- a/lambda-http/src/ext/extensions.rs +++ b/lambda-http/src/ext/extensions.rs @@ -7,12 +7,14 @@ use lambda_runtime::Context; use crate::request::RequestContext; /// ALB/API gateway pre-parsed http query string parameters +#[non_exhaustive] #[derive(Clone)] pub(crate) struct QueryStringParameters(pub(crate) QueryMap); /// API gateway pre-extracted url path parameters /// /// These will always be empty for ALB requests +#[non_exhaustive] #[derive(Clone)] pub(crate) struct PathParameters(pub(crate) QueryMap); @@ -20,10 +22,12 @@ pub(crate) struct PathParameters(pub(crate) QueryMap); /// [stage variables](https://docs.aws.amazon.com/apigateway/latest/developerguide/stage-variables.html) /// /// These will always be empty for ALB requests +#[non_exhaustive] #[derive(Clone)] pub(crate) struct StageVariables(pub(crate) QueryMap); /// ALB/API gateway raw http path without any stage information +#[non_exhaustive] #[derive(Clone)] pub(crate) struct RawHttpPath(pub(crate) String); diff --git a/lambda-http/src/ext/request.rs b/lambda-http/src/ext/request.rs index c56518f66..38e45afa1 100644 --- a/lambda-http/src/ext/request.rs +++ b/lambda-http/src/ext/request.rs @@ -12,6 +12,7 @@ use crate::Body; /// Request payload deserialization errors /// /// Returned by [`RequestPayloadExt::payload()`] +#[non_exhaustive] #[derive(Debug)] pub enum PayloadError { /// Returned when `application/json` bodies fail to deserialize a payload @@ -21,6 +22,7 @@ pub enum PayloadError { } /// Indicates a problem processing a JSON payload. +#[non_exhaustive] #[derive(Debug)] pub enum JsonPayloadError { /// Problem deserializing a JSON payload. @@ -28,6 +30,7 @@ pub enum JsonPayloadError { } /// Indicates a problem processing an x-www-form-urlencoded payload. +#[non_exhaustive] #[derive(Debug)] pub enum FormUrlEncodedPayloadError { /// Problem deserializing an x-www-form-urlencoded payload. diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 92cd5dae1..caea86181 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -1,7 +1,11 @@ #![warn(missing_docs, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //#![deny(warnings)] //! Enriches the `lambda` crate with [`http`](https://github.com/hyperium/http) -//! types targeting AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html), [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations. +//! types targeting AWS +//! * [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) +//! * [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations +//! * [VPC Lattice](https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html) //! //! This crate abstracts over all of these trigger events using standard [`http`](https://github.com/hyperium/http) types minimizing the mental overhead //! of understanding the nuances and variation between trigger details allowing you to focus more on your application while also giving you to the maximum flexibility to @@ -67,9 +71,10 @@ extern crate maplit; pub use http::{self, Response}; /// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. #[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] pub use lambda_runtime::tracing; use lambda_runtime::Diagnostic; -pub use lambda_runtime::{self, service_fn, tower, Context, Error, LambdaEvent, Service}; +pub use lambda_runtime::{self, service_fn, tower, BoxFuture, Context, Error, LambdaEvent, Service, SnapStartResource}; use request::RequestFuture; use response::ResponseFuture; @@ -100,7 +105,9 @@ use std::{ }; mod streaming; -pub use streaming::run_with_streaming_response; +pub use streaming::{run_with_streaming_response, streaming_runtime, StreamAdapter}; +#[cfg(feature = "concurrency-tokio")] +pub use streaming::{run_with_streaming_response_concurrent, streaming_runtime_concurrent}; /// Type alias for `http::Request`s with a fixed [`Body`](enum.Body.html) type pub type Request = http::Request; @@ -108,6 +115,7 @@ pub type Request = http::Request; /// Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] /// /// This is used by the `Adapter` wrapper and is completely internal to the `lambda_http::run` function. +#[non_exhaustive] #[doc(hidden)] pub enum TransformResponse<'a, R, E> { Request(RequestOrigin, RequestFuture<'a, R, E>), @@ -141,12 +149,25 @@ where /// Wraps a `Service` in a `Service>` /// /// This is completely internal to the `lambda_http::run` function. +#[non_exhaustive] #[doc(hidden)] pub struct Adapter<'a, R, S> { service: S, _phantom_data: PhantomData<&'a R>, } +impl<'a, R, S> Clone for Adapter<'a, R, S> +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + service: self.service.clone(), + _phantom_data: PhantomData, + } + } +} + impl<'a, R, S, E> From for Adapter<'a, R, S> where S: Service, @@ -176,9 +197,11 @@ where } fn call(&mut self, req: LambdaEvent) -> Self::Future { - let request_origin = req.payload.request_origin(); - let event: Request = req.payload.into(); - let fut = Box::pin(self.service.call(event.with_lambda_context(req.context))); + let LambdaEvent { payload, context } = req; + let request_origin = payload.request_origin(); + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + let fut = Box::pin(self.service.call(event.with_lambda_context(context))); TransformResponse::Request(request_origin, fut) } @@ -188,7 +211,20 @@ where /// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). /// /// This takes care of transforming the LambdaEvent into a [`Request`] and then -/// converting the result into a [`LambdaResponse`]. +/// converting the result into a `LambdaResponse`. +/// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. +/// If your handler can satisfy `Clone + Send + 'static`, +/// prefer [`run_concurrent`] (requires the `concurrency-tokio` feature), +/// which honors managed concurrency and falls back to sequential behavior when +/// unset. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). pub async fn run<'a, R, S, E>(handler: S) -> Result<(), Error> where S: Service, @@ -199,6 +235,138 @@ where lambda_runtime::run(Adapter::from(handler)).await } +/// Starts the Lambda Rust runtime and begins polling for events on the [Lambda +/// Runtime APIs](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html). +/// +/// This takes care of transforming the LambdaEvent into a [`Request`] and then +/// converting the result into a `LambdaResponse`. +/// +/// # Managed concurrency +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// function spawns multiple tokio worker tasks to handle concurrent invocations. +/// When the environment variable is unset or `<= 1`, it falls back to +/// sequential behavior, so the same handler can run on both classic Lambda +/// and Lambda Managed Instances. +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime with `AWS_LAMBDA_MAX_CONCURRENCY > 1` +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_concurrent(handler: S) -> Result<(), Error> +where + S: Service + Clone + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::run_concurrent(Adapter::from(handler)).await +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// handler, without starting the event loop. +/// +/// Use this when you need to register +/// [`SnapStartResource`]s for the SnapStart +/// snapshot/restore lifecycle. For the common case where no custom hooks are +/// needed, use [`run()`] instead — SnapStart support works automatically. +/// +/// # Example +/// +/// ```no_run +/// use lambda_http::{service_fn, BoxFuture, Error, Request, SnapStartResource}; +/// use std::sync::Arc; +/// +/// struct Pool; +/// impl SnapStartResource for Pool { +/// fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { +/// Box::pin(async move { Ok(()) }) +/// } +/// fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { +/// Box::pin(async move { Ok(()) }) +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let pool = Arc::new(Pool); +/// let runtime = lambda_http::runtime(service_fn(handler)) +/// .register_snapstart_resource(pool.clone()); +/// +/// runtime.run().await?; +/// Ok(()) +/// } +/// +/// async fn handler(_req: Request) -> Result<&'static str, std::convert::Infallible> { +/// Ok("hello") +/// } +/// ``` +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). +pub fn runtime( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service, +> +where + S: Service + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::Runtime::new(Adapter::from(handler)) +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// handler for concurrent execution, without starting the event loop. +/// +/// This is the concurrent variant of [`runtime()`]. Use it when you need SnapStart +/// hooks AND your handler supports concurrent invocations. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub fn runtime_concurrent( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service< + lambda_runtime::LambdaInvocation, + Response = (), + Error = lambda_runtime::Error, + Future: Send, + > + Clone + + Send + + 'static, +> +where + S: Service + Clone + Send + 'static, + S::Future: Send + 'static, + R: IntoResponse + Send + Sync + 'static, + E: std::fmt::Debug + Into + Send + 'static, +{ + lambda_runtime::Runtime::new(Adapter::from(handler)) +} + +// In concurrent mode we must use the per-request context. +fn update_xray_trace_id_header(headers: &mut http::HeaderMap, context: &Context) { + if let Some(trace_id) = context.xray_trace_id.as_deref() { + if let Ok(header_value) = http::HeaderValue::from_str(trace_id) { + headers.insert(http::header::HeaderName::from_static("x-amzn-trace-id"), header_value); + } + } +} + #[cfg(test)] mod test_adapter { use std::task::{Context, Poll}; @@ -253,4 +421,35 @@ mod test_adapter { .service_fn(|_event: Request| async move { Response::builder().status(StatusCode::OK).body(Body::Empty) }) .boxed(); } + + async fn http_handler(_req: Request) -> Result<&'static str, std::convert::Infallible> { + Ok("hello") + } + + /// `runtime()` accepts a (non-`Clone`) handler for the sequential path. This + /// is a compile-time check that the helper's bounds are satisfiable. + #[test] + fn runtime_helper_accepts_handler() { + let _ = || { + let _runtime = crate::runtime(crate::service_fn(http_handler)); + }; + } + + /// `runtime_concurrent()` requires a `Clone` handler (Lambda Managed + /// Instances). `service_fn` over an `async fn` is `Clone`, so this compiles; + /// it guards against the `Clone` bound regressing on the concurrent helper. + /// + /// Crucially, this also calls `.run_concurrent()` on the result: that method + /// requires the wrapped service to be `Clone + Send + 'static` with a `Send` + /// future, so this compiling proves the helper's return type exposes those + /// bounds (without them, the chained call would not type-check). + #[cfg(feature = "concurrency-tokio")] + #[test] + fn runtime_concurrent_helper_exposes_lmi_bounds() { + let _ = || async { + let _ = crate::runtime_concurrent(crate::service_fn(http_handler)) + .run_concurrent() + .await; + }; + } } diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index a9281b461..e5f784769 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -1,6 +1,6 @@ -//! ALB and API Gateway request adaptations +//! ALB and API Gateway and VPC Lattice request adaptations //! -//! Typically these are exposed via the [`request_context()`] or [`request_context_ref()`] +//! Typically, these are exposed via the [`request_context()`] or [`request_context_ref()`] //! request extension methods provided by the [`RequestExt`] trait. //! //! [`request_context()`]: crate::RequestExt::request_context() @@ -12,7 +12,8 @@ use crate::ext::extensions::{PathParameters, StageVariables}; feature = "apigw_rest", feature = "apigw_http", feature = "alb", - feature = "apigw_websockets" + feature = "apigw_websockets", + feature = "vpc_lattice" ))] use crate::ext::extensions::{QueryStringParameters, RawHttpPath}; #[cfg(feature = "alb")] @@ -25,6 +26,9 @@ use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyRequestCon use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext}; #[cfg(feature = "apigw_websockets")] use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext}; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::{VpcLatticeRequestV2, VpcLatticeRequestV2Context}; + use aws_lambda_events::{encodings::Body, query_map::QueryMap}; use http::{header::HeaderName, HeaderMap, HeaderValue}; @@ -35,10 +39,11 @@ use std::{env, future::Future, io::Read, pin::Pin}; use url::Url; /// Internal representation of an Lambda http event from -/// ALB, API Gateway REST and HTTP API proxy event perspectives +/// ALB, VPC Lattice Lambda, API Gateway REST and HTTP API proxy event perspectives /// /// This is not intended to be a type consumed by crate users directly. The order /// of the variants are notable. Serde will try to deserialize in this order. +#[non_exhaustive] #[doc(hidden)] #[derive(Debug)] pub enum LambdaRequest { @@ -50,6 +55,8 @@ pub enum LambdaRequest { Alb(AlbTargetGroupRequest), #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequest), + #[cfg(feature = "vpc_lattice")] + VpcLatticeV2(VpcLatticeRequestV2), #[cfg(feature = "pass_through")] PassThrough(String), } @@ -68,15 +75,18 @@ impl LambdaRequest { LambdaRequest::Alb { .. } => RequestOrigin::Alb, #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket { .. } => RequestOrigin::WebSocket, + #[cfg(feature = "vpc_lattice")] + LambdaRequest::VpcLatticeV2 { .. } => RequestOrigin::VpcLatticeV2, #[cfg(feature = "pass_through")] LambdaRequest::PassThrough { .. } => RequestOrigin::PassThrough, #[cfg(not(any( feature = "apigw_rest", feature = "apigw_http", feature = "alb", - feature = "apigw_websockets" + feature = "apigw_websockets", + feature = "vpc_lattice", )))] - _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, or `apigw_websockets` must be enabled for the `lambda-http` crate."), + _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, `apigw_websockets` or `vpc_lattice` must be enabled for the `lambda-http` crate."), } } } @@ -85,6 +95,7 @@ impl LambdaRequest { pub type RequestFuture<'a, R, E> = Pin> + Send + 'a>>; /// Represents the origin from which the lambda was requested from. +#[non_exhaustive] #[doc(hidden)] #[derive(Debug, Clone)] pub enum RequestOrigin { @@ -100,6 +111,9 @@ pub enum RequestOrigin { /// API Gateway WebSocket #[cfg(feature = "apigw_websockets")] WebSocket, + /// VPC Lattice origin + #[cfg(feature = "vpc_lattice")] + VpcLatticeV2, /// PassThrough request origin #[cfg(feature = "pass_through")] PassThrough, @@ -119,7 +133,7 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request http::Request http::Request http::Request { let http_method = ag.http_method; @@ -212,7 +218,6 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request { // multi-value_headers our cannoncial source of request headers let mut headers = ag.multi_value_headers; headers.extend(ag.headers); - update_xray_trace_id_header(&mut headers); let base64 = ag.is_base64_encoded; let mut req = builder @@ -263,7 +268,6 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { // multi-value_headers our cannoncial source of request headers let mut headers = alb.multi_value_headers; headers.extend(alb.headers); - update_xray_trace_id_header(&mut headers); let base64 = alb.is_base64_encoded; @@ -282,7 +286,7 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { req } -#[cfg(feature = "alb")] +#[cfg(any(feature = "alb", feature = "vpc_lattice"))] fn decode_query_map(query_map: QueryMap) -> QueryMap { use std::str::FromStr; @@ -328,7 +332,6 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< // multi-value_headers our canonical source of request headers let mut headers = ag.multi_value_headers; headers.extend(ag.headers); - update_xray_trace_id_header(&mut headers); let base64 = ag.is_base64_encoded; let mut req = builder @@ -346,6 +349,42 @@ fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request< req } +#[cfg(feature = "vpc_lattice")] +fn into_vpc_lattice_request(vlr: VpcLatticeRequestV2) -> http::Request { + let http_method = vlr.method; + let host = vlr.headers.get(http::header::HOST).and_then(|s| s.to_str().ok()); + let raw_path = vlr.path.unwrap_or_default(); + + let query_string_parameters = decode_query_map(vlr.query_string_parameters); + + let builder = http::Request::builder() + .uri(build_request_uri( + &raw_path, + &vlr.headers, + host, + Some((&query_string_parameters, &query_string_parameters)), + )) + .extension(RawHttpPath(raw_path)) + .extension(QueryStringParameters(query_string_parameters)) + .extension(RequestContext::VpcLattice(vlr.request_context)); + + let base64 = vlr.is_base64_encoded; + + let mut req = builder + .body( + vlr.body + .as_deref() + .map_or_else(Body::default, |b| Body::from_maybe_encoded(base64, b)), + ) + .expect("failed to build request"); + + // no builder method that sets headers in batch + let _ = std::mem::replace(req.headers_mut(), vlr.headers); + let _ = std::mem::replace(req.method_mut(), http_method.unwrap_or(http::Method::GET)); + + req +} + #[cfg(feature = "pass_through")] fn into_pass_through_request(data: String) -> http::Request { let mut builder = http::Request::builder(); @@ -353,8 +392,6 @@ fn into_pass_through_request(data: String) -> http::Request { let headers = builder.headers_mut().unwrap(); headers.insert("Content-Type", "application/json".parse().unwrap()); - update_xray_trace_id_header(headers); - let raw_path = "/events"; builder @@ -388,6 +425,7 @@ fn apigw_path_with_stage(stage: &Option, path: &str) -> String { /// Event request context as an enumeration of request contexts /// for both ALB and API Gateway and HTTP API events +#[non_exhaustive] #[derive(Deserialize, Debug, Clone, Serialize)] #[serde(untagged)] pub enum RequestContext { @@ -403,6 +441,9 @@ pub enum RequestContext { /// WebSocket request context #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequestContext), + /// VPC Lattice request context + #[cfg(feature = "vpc_lattice")] + VpcLattice(VpcLatticeRequestV2Context), /// Custom request context #[cfg(feature = "pass_through")] PassThrough, @@ -420,6 +461,8 @@ impl From for http::Request { LambdaRequest::Alb(alb) => into_alb_request(alb), #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket(ag) => into_websocket_request(ag), + #[cfg(feature = "vpc_lattice")] + LambdaRequest::VpcLatticeV2(vpclat) => into_vpc_lattice_request(vpclat), #[cfg(feature = "pass_through")] LambdaRequest::PassThrough(data) => into_pass_through_request(data), } @@ -437,7 +480,7 @@ impl RequestContext { Self::ApiGatewayV2(ag) => ag.authorizer.as_ref(), #[cfg(feature = "apigw_websockets")] Self::WebSocket(ag) => Some(&ag.authorizer), - #[cfg(any(feature = "alb", feature = "pass_through"))] + #[cfg(any(feature = "alb", feature = "pass_through", feature = "vpc_lattice"))] _ => None, } } @@ -580,7 +623,7 @@ mod tests { assert_eq!(req.method(), "POST"); assert_eq!(req.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1¶meter1=value2¶meter2=value"); - assert_eq!(cookie_header, Ok("cookie1=value1;cookie2=value2")); + assert_eq!(cookie_header, Ok("cookie1=value1; cookie2=value2")); // Ensure this is an APIGWv2 request let req_context = req.request_context_ref().expect("Request is missing RequestContext"); @@ -783,6 +826,193 @@ mod tests { ); } + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_basic() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + + let body_str = match request.body() { + Body::Text(s) => s.as_str(), + _ => "", + }; + + assert_eq!(body_str, "All is good"); + + let uri = request.uri().to_string(); + assert!(uri.starts_with("/health?"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=a"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=DEF"), "unexpected uri: {uri}"); + assert!(uri.contains("multi=g"), "unexpected uri: {uri}"); + assert!(uri.contains("state=prod"), "unexpected uri: {uri}"); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_basic_base64() { + let input = include_str!("../tests/data/vpc_lattice_v2_request_base64.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + + let body_array = match request.body() { + Body::Binary(s) => s.as_slice(), + _ => &[], + }; + + assert_eq!(body_array, *b"All is good"); + + // URI should have been built from host header, query and protocol etc + let uri = request.uri(); + + assert!(uri.to_string().starts_with("https://www.site.com/health?")); + assert!(uri.to_string().contains("multi=a&multi=DEF&multi=g")); + assert!(uri.to_string().contains("state=prod")); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_headers() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + + // decoding multi value headers + let multi_headers_as_big_string = request + .headers() + .get_all("multi") + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .reduce(|acc, nxt| [acc, nxt].join(";")); + + assert_eq!(multi_headers_as_big_string, Some("x;y".to_string())); + + // decoding regular headers + let basic_headers_as_big_string = request + .headers() + .get_all("user-agent") + .iter() + .map(|v| v.to_str().unwrap().to_string()) + .reduce(|acc, nxt| [acc, nxt].join(";")); + + assert_eq!(basic_headers_as_big_string, Some("curl/7.68.0".to_string())); + + // Ensure this is an VPC Lattice request + let req_context = request + .request_context_ref() + .expect("Request is missing RequestContext"); + assert!( + matches!(req_context, &RequestContext::VpcLattice(_)), + "expected Vpc lattice context, got {req_context:?}" + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_multi_value_querys() { + let input = include_str!("../tests/data/vpc_lattice_v2_request.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + assert!(!request + .query_string_parameters_ref() + .expect("Request is missing query parameters") + .is_empty()); + + let params = request.query_string_parameters(); + assert_eq!(Some(vec!["prod"]), params.all("state")); + assert_eq!(Some(vec!["a", "DEF", "g"]), params.all("multi")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("multi=a&multi=DEF&multi=g")); + assert!(query.contains("state=prod")); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_encoded_query_parameters() { + let input = include_str!("../tests/data/vpc_lattice_v2_request_encoded_query.json"); + let result = from_str(input); + assert!( + result.is_ok(), + "event is was not parsed as expected {result:?} given {input}" + ); + let request = result.expect("failed to parse request"); + + let params = request.query_string_parameters(); + // percent-encoded values should be decoded + assert_eq!(Some(vec!["?showAll=true"]), params.all("filter")); + assert_eq!(Some(vec!["hello world"]), params.all("q")); + + let query = request.uri().query().unwrap(); + assert!(query.contains("filter="), "unexpected uri query: {query}"); + assert!(query.contains("q="), "unexpected uri query: {query}"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn deserializes_vpc_lattice_no_body() { + let input = r#"{ + "version": "2.0", + "path": "/ping", + "method": "GET", + "headers": {"accept": ["*/*"]}, + "queryStringParameters": {}, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + assert!( + matches!(request.body(), Body::Empty), + "expected empty body, got {:?}", + request.body() + ); + } + #[test] fn deserialize_apigw_http_sam_local() { // manually generated from AWS SAM CLI @@ -954,4 +1184,65 @@ mod tests { let authorizer = req_context.authorizer().expect("authorizer is missing"); assert_eq!(Some("admin"), authorizer.fields.get("principalId").unwrap().as_str()); } + + #[test] + #[cfg(all(feature = "apigw_http", feature = "vpc_lattice"))] + fn vpc_lattice_event_does_not_match_apigw_v2() { + let data = include_bytes!("../../lambda-events/src/fixtures/example-vpc-lattice-v2-request.json"); + let result = serde_json::from_slice::(data); + assert!(result.is_err(), "VPC Lattice event should not deserialize as APIGW V2"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn vpc_lattice_method_none_defaults_to_get() { + let input = r#"{ + "version": "2.0", + "path": "/ping", + "headers": {"accept": ["*/*"]}, + "queryStringParameters": {}, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "GET"); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn vpc_lattice_post_with_body() { + let input = r#"{ + "version": "2.0", + "path": "/submit", + "method": "POST", + "headers": {"content-type": ["application/json"]}, + "queryStringParameters": {}, + "body": "{\"key\":\"value\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:us-east-1:123456789012:servicenetwork/sn-abc", + "serviceArn": "arn:aws:vpc-lattice:us-east-1:123456789012:service/svc-abc", + "targetGroupArn": "arn:aws:vpc-lattice:us-east-1:123456789012:targetgroup/tg-abc", + "region": "us-east-1", + "timeEpoch": "1724875399456789" + } + }"#; + let result = from_str(input); + assert!(result.is_ok(), "event was not parsed as expected {result:?}"); + let request = result.expect("failed to parse request"); + assert_eq!(request.method(), "POST"); + let body_str = match request.body() { + Body::Text(s) => s.as_str(), + other => panic!("expected text body, got {other:?}"), + }; + assert_eq!(body_str, r#"{"key":"value"}"#); + } } diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index e8528fdf2..da3873225 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -8,6 +8,8 @@ use aws_lambda_events::apigw::ApiGatewayProxyResponse; #[cfg(feature = "apigw_http")] use aws_lambda_events::apigw::ApiGatewayV2httpResponse; use aws_lambda_events::encodings::Body; +#[cfg(feature = "vpc_lattice")] +use aws_lambda_events::vpc_lattice::VpcLatticeResponse; use encoding_rs::Encoding; use http::{ header::{CONTENT_ENCODING, CONTENT_TYPE}, @@ -40,6 +42,7 @@ const TEXT_ENCODING_PREFIXES: [&str; 5] = [ const TEXT_ENCODING_SUFFIXES: [&str; 3] = ["+xml", "+yaml", "+json"]; /// Representation of Lambda response +#[non_exhaustive] #[doc(hidden)] #[derive(Serialize, Debug)] #[serde(untagged)] @@ -50,6 +53,8 @@ pub enum LambdaResponse { ApiGatewayV2(ApiGatewayV2httpResponse), #[cfg(feature = "alb")] Alb(AlbTargetGroupResponse), + #[cfg(feature = "vpc_lattice")] + VpcLattice(VpcLatticeResponse), #[cfg(feature = "pass_through")] PassThrough(serde_json::Value), } @@ -62,6 +67,7 @@ impl LambdaResponse { Body::Empty => (false, None), b @ Body::Text(_) => (false, Some(b)), b @ Body::Binary(_) => (true, Some(b)), + _ => (false, None), }; let headers = parts.headers; @@ -69,14 +75,22 @@ impl LambdaResponse { match request_origin { #[cfg(feature = "apigw_rest")] - RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { - body, - is_base64_encoded, - status_code: status_code as i64, + RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1({ + let mut response = ApiGatewayProxyResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; // Explicitly empty, as API gateway v1 will merge "headers" and // "multi_value_headers" fields together resulting in duplicate response headers. - headers: HeaderMap::new(), - multi_value_headers: headers, + response.headers = HeaderMap::new(); + response.multi_value_headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), #[cfg(feature = "apigw_http")] RequestOrigin::ApiGatewayV2 => { @@ -87,47 +101,83 @@ impl LambdaResponse { let cookies = headers .get_all(SET_COOKIE) .iter() - .cloned() .map(|v| v.to_str().ok().unwrap_or_default().to_string()) .collect(); headers.remove(SET_COOKIE); - LambdaResponse::ApiGatewayV2(ApiGatewayV2httpResponse { - body, - is_base64_encoded, - status_code: status_code as i64, - cookies, + LambdaResponse::ApiGatewayV2({ + let mut response = ApiGatewayV2httpResponse::default(); + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; + response.cookies = cookies; // API gateway v2 doesn't have "multi_value_headers" field. Duplicate headers // are combined with commas and included in the headers field. - headers, - multi_value_headers: HeaderMap::new(), + response.headers = headers; + response.multi_value_headers = HeaderMap::new(); + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }) } #[cfg(feature = "alb")] - RequestOrigin::Alb => LambdaResponse::Alb(AlbTargetGroupResponse { - body, - status_code: status_code as i64, - is_base64_encoded, + RequestOrigin::Alb => LambdaResponse::Alb({ + let mut response = AlbTargetGroupResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; // ALB responses are used for ALB integration, which can be configured to use // either "headers" or "multi_value_headers" field. We need to return both fields // to ensure both configuration work correctly. - headers: headers.clone(), - multi_value_headers: headers, - status_description: Some(format!( + response.headers = headers.clone(); + response.multi_value_headers = headers; + response.status_description = Some(format!( "{} {}", status_code, parts.status.canonical_reason().unwrap_or_default() - )), + )); + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), #[cfg(feature = "apigw_websockets")] - RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { - body, - is_base64_encoded, - status_code: status_code as i64, + RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1({ + let mut response = ApiGatewayProxyResponse::default(); + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code as i64; // Explicitly empty, as API gateway v1 will merge "headers" and // "multi_value_headers" fields together resulting in duplicate response headers. - headers: HeaderMap::new(), - multi_value_headers: headers, + response.headers = HeaderMap::new(); + response.multi_value_headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response + }), + #[cfg(feature = "vpc_lattice")] + RequestOrigin::VpcLatticeV2 => LambdaResponse::VpcLattice({ + let mut response = VpcLatticeResponse::default(); + + response.body = body; + response.is_base64_encoded = is_base64_encoded; + response.status_code = status_code; + response.headers = headers; + // Today, this implementation doesn't provide any additional fields + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }), #[cfg(feature = "pass_through")] RequestOrigin::PassThrough => { @@ -142,9 +192,10 @@ impl LambdaResponse { feature = "apigw_rest", feature = "apigw_http", feature = "alb", - feature = "apigw_websockets" + feature = "apigw_websockets", + feature = "vpc_lattice" )))] - _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, or `apigw_websockets` must be enabled for the `lambda-http` crate."), + _ => compile_error!("Either feature `apigw_rest`, `apigw_http`, `alb`, `apigw_websockets` or `vpc_lattice` must be enabled for the `lambda-http` crate."), } } } @@ -153,7 +204,7 @@ impl LambdaResponse { /// /// Types that implement this trait can be used as return types for handler functions. pub trait IntoResponse { - /// Transform into a Response Future + /// Transform into a `Response` Future fn into_response(self) -> ResponseFuture; } @@ -553,6 +604,56 @@ mod tests { ) } + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_text_body() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(Body::from("hello")) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"isBase64Encoded":false,"statusCode":200,"headers":{"content-type":"text/plain"},"body":"hello"}"# + ); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_binary_body() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(200) + .body(Body::from(b"hello".as_slice())) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!(json, r#"{"isBase64Encoded":true,"statusCode":200,"body":"aGVsbG8="}"#); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn serialize_vpc_lattice_response_non_200_status() { + let res = LambdaResponse::from_response( + &RequestOrigin::VpcLatticeV2, + Response::builder() + .status(404) + .header("x-request-id", "abc-123") + .body(Body::from(())) + .expect("failed to create response"), + ); + let json = serde_json::to_string(&res).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"isBase64Encoded":false,"statusCode":404,"headers":{"x-request-id":"abc-123"},"body":null}"# + ); + } + #[test] fn serialize_multi_value_headers() { let res = LambdaResponse::from_response( diff --git a/lambda-http/src/streaming.rs b/lambda-http/src/streaming.rs index a93408b41..361a925e1 100644 --- a/lambda-http/src/streaming.rs +++ b/lambda-http/src/streaming.rs @@ -1,22 +1,103 @@ -use crate::{http::header::SET_COOKIE, request::LambdaRequest, tower::ServiceBuilder, Request, RequestExt}; +use crate::{http::header::SET_COOKIE, request::LambdaRequest, update_xray_trace_id_header, Request, RequestExt}; use bytes::Bytes; -pub use http::{self, Response}; -use http_body::Body; -use lambda_runtime::Diagnostic; -pub use lambda_runtime::{self, tower::ServiceExt, Error, LambdaEvent, MetadataPrelude, Service, StreamResponse}; -use std::{ +use core::{ fmt::Debug, pin::Pin, task::{Context, Poll}, }; -use tokio_stream::Stream; +use futures_util::{Stream, TryFutureExt}; +pub use http::{self, Response}; +use http_body::Body; +use lambda_runtime::{ + tower::{ + util::{MapRequest, MapResponse}, + ServiceBuilder, ServiceExt, + }, + Diagnostic, +}; +pub use lambda_runtime::{Error, LambdaEvent, MetadataPrelude, Service, StreamResponse}; +use std::{future::Future, marker::PhantomData}; + +/// An adapter that lifts a standard [`Service`] into a +/// [`Service>`] which produces streaming Lambda HTTP +/// responses. +#[non_exhaustive] +pub struct StreamAdapter<'a, S, B> { + service: S, + _phantom_data: PhantomData<&'a B>, +} -/// Starts the Lambda Rust runtime and stream response back [Configure Lambda -/// Streaming Response](https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html). +impl<'a, S, B> Clone for StreamAdapter<'a, S, B> +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + service: self.service.clone(), + _phantom_data: PhantomData, + } + } +} + +impl<'a, S, B, E> From for StreamAdapter<'a, S, B> +where + S: Service, Error = E>, + S::Future: Send + 'a, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + fn from(service: S) -> Self { + StreamAdapter { + service, + _phantom_data: PhantomData, + } + } +} + +impl<'a, S, B, E> Service> for StreamAdapter<'a, S, B> +where + S: Service, Error = E>, + S::Future: Send + 'a, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + type Response = StreamResponse>; + type Error = E; + type Future = Pin> + Send + 'a>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, req: LambdaEvent) -> Self::Future { + let LambdaEvent { payload, context } = req; + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + Box::pin( + self.service + .call(event.with_lambda_context(context)) + .map_ok(into_stream_response), + ) + } +} + +/// Builds a streaming-aware Tower service from a `Service` **without** +/// boxing its future (no heap allocation / vtable). /// -/// This takes care of transforming the LambdaEvent into a [`Request`] and -/// accepts [`http::Response`] as response. -pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(), Error> +/// Transforms `LambdaEvent` into `Request` with Lambda context +/// and wraps `Response` into `StreamResponse>`. +/// +/// Used internally by [`run_with_streaming_response`]; not part of the public +/// API. +#[allow(clippy::type_complexity)] +fn into_stream_service<'a, S, B, E>( + handler: S, +) -> MapResponse< + MapRequest) -> Request>, + impl FnOnce(Response) -> StreamResponse> + Clone, +> where S: Service, Error = E>, S::Future: Send + 'a, @@ -25,41 +106,190 @@ where B::Data: Into + Send, B::Error: Into + Send + Debug, { - let svc = ServiceBuilder::new() - .map_request(|req: LambdaEvent| { - let event: Request = req.payload.into(); - event.with_lambda_context(req.context) - }) + ServiceBuilder::new() + .map_request(event_to_request as fn(LambdaEvent) -> Request) .service(handler) - .map_response(|res| { - let (parts, body) = res.into_parts(); + .map_response(into_stream_response) +} - let mut prelude_headers = parts.headers; +/// Builds a streaming-aware Tower service from a `Service` that can be +/// cloned and sent across tasks. This is used by the concurrent HTTP entrypoint. +#[cfg(feature = "concurrency-tokio")] +type EventToRequest = fn(LambdaEvent) -> Request; - let cookies = prelude_headers.get_all(SET_COOKIE); - let cookies = cookies - .iter() - .map(|c| String::from_utf8_lossy(c.as_bytes()).to_string()) - .collect::>(); +#[cfg(feature = "concurrency-tokio")] +#[allow(clippy::type_complexity)] +fn into_stream_service_cloneable( + handler: S, +) -> MapResponse, impl FnOnce(Response) -> StreamResponse> + Clone> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + ServiceBuilder::new() + .map_request(event_to_request as EventToRequest) + .service(handler) + .map_response(into_stream_response) +} - prelude_headers.remove(SET_COOKIE); +/// Converts an `http::Response` into a streaming Lambda response. +fn into_stream_response(res: Response) -> StreamResponse> +where + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + let (parts, body) = res.into_parts(); - let metadata_prelude = MetadataPrelude { - headers: prelude_headers, - status_code: parts.status, - cookies, - }; + let mut headers = parts.headers; + let cookies = headers + .get_all(SET_COOKIE) + .iter() + .map(|c| String::from_utf8_lossy(c.as_bytes()).to_string()) + .collect::>(); + headers.remove(SET_COOKIE); - StreamResponse { - metadata_prelude, - stream: BodyStream { body }, - } - }); + StreamResponse { + metadata_prelude: MetadataPrelude { + headers, + status_code: parts.status, + cookies, + }, + stream: BodyStream { body }, + } +} - lambda_runtime::run(svc).await +fn event_to_request(req: LambdaEvent) -> Request { + let LambdaEvent { payload, context } = req; + let mut event: Request = payload.into(); + update_xray_trace_id_header(event.headers_mut(), &context); + event.with_lambda_context(context) +} + +/// Runs the Lambda runtime with a handler that returns **streaming** HTTP +/// responses. +/// +/// See the [AWS docs for response streaming]. +/// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, this function returns an error because +/// it does not enable concurrent polling. Use [`run_with_streaming_response_concurrent`] +/// (requires the `concurrency-tokio` feature) instead. +/// +/// [AWS docs for response streaming]: +/// https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). +pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(), Error> +where + S: Service, Error = E>, + S::Future: Send + 'a, + E: Debug + Into, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::run(into_stream_service(handler)).await +} + +/// Runs the Lambda runtime with a handler that returns **streaming** HTTP +/// responses, in a mode that is compatible with Lambda Managed Instances. +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// spawns multiple tokio worker tasks to handle concurrent invocations. When the +/// environment variable is unset or `<= 1`, it falls back to sequential +/// behavior, so the same handler can run on both classic Lambda and Lambda +/// Managed Instances. +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_with_streaming_response_concurrent(handler: S) -> Result<(), Error> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::run_concurrent(into_stream_service_cloneable(handler)).await +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// streaming handler, without starting the event loop. +/// +/// Use this when you need access to the [`Runtime`](lambda_runtime::Runtime) for +/// SnapStart lifecycle hooks with a streaming response handler. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +pub fn streaming_runtime( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service, +> +where + S: Service, Error = E> + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::Runtime::new(into_stream_service(handler)) +} + +/// Returns a configured [`Runtime`](lambda_runtime::Runtime) wrapping the given +/// streaming handler for concurrent execution, without starting the event loop. +/// +/// This is the concurrent variant of [`streaming_runtime()`]. +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing. +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub fn streaming_runtime_concurrent( + handler: S, +) -> lambda_runtime::Runtime< + impl lambda_runtime::Service< + lambda_runtime::LambdaInvocation, + Response = (), + Error = lambda_runtime::Error, + Future: Send, + > + Clone + + Send + + 'static, +> +where + S: Service, Error = E> + Clone + Send + 'static, + S::Future: Send + 'static, + E: Debug + Into + Send + 'static, + B: Body + Unpin + Send + 'static, + B::Data: Into + Send, + B::Error: Into + Send + Debug, +{ + lambda_runtime::Runtime::new(into_stream_service_cloneable(handler)) } pin_project_lite::pin_project! { +#[non_exhaustive] pub struct BodyStream { #[pin] pub(crate) body: B, @@ -85,3 +315,47 @@ where } } } + +#[cfg(test)] +mod test_stream_adapter { + use super::*; + + use crate::Body; + use http::StatusCode; + + // A middleware that logs requests before forwarding them to another service + struct LogService { + inner: S, + } + + impl Service> for LogService + where + S: Service>, + { + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, event: LambdaEvent) -> Self::Future { + println!("Lambda event: {event:#?}"); + self.inner.call(event) + } + } + + #[test] + fn stream_adapter_is_boxable() { + // Works with a concrete service stack (no boxing) + let svc = ServiceBuilder::new() + .layer_fn(|service| LogService { inner: service }) + .layer_fn(StreamAdapter::from) + .service_fn( + |_req: Request| async move { http::Response::builder().status(StatusCode::OK).body(Body::Empty) }, + ); + // Also works when the stack is boxed (type-erased) + let _boxed_svc = svc.boxed(); + } +} diff --git a/lambda-http/tests/data/vpc_lattice_v2_request.json b/lambda-http/tests/data/vpc_lattice_v2_request.json new file mode 100644 index 000000000..2ac3920f9 --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": "All is good", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-http/tests/data/vpc_lattice_v2_request_base64.json b/lambda-http/tests/data/vpc_lattice_v2_request_base64.json new file mode 100644 index 000000000..c622a775f --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request_base64.json @@ -0,0 +1,31 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "host": "www.site.com", + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": "QWxsIGlzIGdvb2Q=", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json b/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json new file mode 100644 index 000000000..a848638e3 --- /dev/null +++ b/lambda-http/tests/data/vpc_lattice_v2_request_encoded_query.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "path": "/search", + "method": "GET", + "headers": { + "host": ["api.example.com"], + "accept": ["*/*"] + }, + "queryStringParameters": { + "filter": ["%3FshowAll%3Dtrue"], + "q": ["hello%20world"] + }, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-integration-tests/Cargo.toml b/lambda-integration-tests/Cargo.toml index ee44a9694..e2497333e 100644 --- a/lambda-integration-tests/Cargo.toml +++ b/lambda-integration-tests/Cargo.toml @@ -1,25 +1,29 @@ [package] -name = "aws_lambda_rust_integration_tests" -version = "0.1.0" +name = "lambda-integration-tests" +version = "1.0.1" authors = ["Maxime David"] edition = "2021" +rust-version = "1.84.0" description = "AWS Lambda Runtime integration tests" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "../README.md" [dependencies] -lambda_runtime = { path = "../lambda-runtime" } +lambda_runtime = { path = "../lambda-runtime", features = ["tracing", "graceful-shutdown"] } aws_lambda_events = { path = "../lambda-events" } serde_json = "1.0.121" tokio = { version = "1", features = ["full"] } serde = { version = "1.0.204", features = ["derive"] } +tracing = "0.1" [dev-dependencies] -reqwest = { version = "0.12.5", features = ["blocking"] } -openssl = { version = "0.10", features = ["vendored"] } +reqwest = { version = "0.13.1", features = ["blocking"] } + +[features] +catch-all-fields = ["aws_lambda_events/catch-all-fields"] [[bin]] name = "helloworld" @@ -28,3 +32,7 @@ path = "src/helloworld.rs" [[bin]] name = "authorizer" path = "src/authorizer.rs" + +[[bin]] +name = "tenant-id-test" +path = "src/tenant_id_test.rs" diff --git a/lambda-integration-tests/samconfig.toml b/lambda-integration-tests/samconfig.toml index 0212b62ae..f3f49b5ae 100644 --- a/lambda-integration-tests/samconfig.toml +++ b/lambda-integration-tests/samconfig.toml @@ -3,7 +3,8 @@ version = 0.1 [default] [default.build.parameters] cached = true -parallel = true +parallel = false +build_in_source = true [default.validate.parameters] lint = true @@ -11,7 +12,7 @@ lint = true [default.deploy.parameters] capabilities = "CAPABILITY_IAM" confirm_changeset = true -s3_bucket = "aws-lambda-rust-runtime-integration-testing" +resolve_s3 = true [default.sync.parameters] watch = true diff --git a/lambda-integration-tests/src/authorizer.rs b/lambda-integration-tests/src/authorizer.rs index 41ddd2d8e..23f17b296 100644 --- a/lambda-integration-tests/src/authorizer.rs +++ b/lambda-integration-tests/src/authorizer.rs @@ -34,20 +34,36 @@ async fn func( } fn allow(method_arn: &str) -> ApiGatewayCustomAuthorizerResponse { - let stmt = IamPolicyStatement { - action: vec!["execute-api:Invoke".to_string()], - resource: vec![method_arn.to_owned()], - effect: aws_lambda_events::iam::IamPolicyEffect::Allow, - condition: None, + let stmt = { + let mut statement = IamPolicyStatement::default(); + statement.action = vec!["execute-api:Invoke".to_string()]; + statement.resource = vec![method_arn.to_owned()]; + statement.effect = aws_lambda_events::iam::IamPolicyEffect::Allow; + statement.condition = None; + #[cfg(feature = "catch-all-fields")] + { + statement.other = Default::default(); + } + statement }; - let policy = ApiGatewayCustomAuthorizerPolicy { - version: Some("2012-10-17".to_string()), - statement: vec![stmt], + let policy = { + let mut policy = ApiGatewayCustomAuthorizerPolicy::default(); + policy.version = Some("2012-10-17".to_string()); + policy.statement = vec![stmt]; + #[cfg(feature = "catch-all-fields")] + { + policy.other = Default::default(); + } + policy }; - ApiGatewayCustomAuthorizerResponse { - principal_id: Some("user".to_owned()), - policy_document: policy, - context: json!({ "hello": "world" }), - usage_identifier_key: None, + let mut response = ApiGatewayCustomAuthorizerResponse::default(); + response.principal_id = Some("user".to_owned()); + response.policy_document = policy; + response.context = json!({ "hello": "world" }); + response.usage_identifier_key = None; + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); } + response } diff --git a/lambda-integration-tests/src/helloworld.rs b/lambda-integration-tests/src/helloworld.rs index 9989da438..2eafc409a 100644 --- a/lambda-integration-tests/src/helloworld.rs +++ b/lambda-integration-tests/src/helloworld.rs @@ -8,6 +8,7 @@ use lambda_runtime::{service_fn, tracing, Error, LambdaEvent}; async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); let func = service_fn(func); + lambda_runtime::spawn_graceful_shutdown_handler(|| async move {}).await; lambda_runtime::run(func).await?; Ok(()) } @@ -15,12 +16,18 @@ async fn main() -> Result<(), Error> { async fn func(_event: LambdaEvent) -> Result { let mut headers = HeaderMap::new(); headers.insert("content-type", "text/html".parse().unwrap()); - let resp = ApiGatewayProxyResponse { - status_code: 200, - multi_value_headers: headers.clone(), - is_base64_encoded: false, - body: Some("Hello world!".into()), - headers, + let resp = { + let mut response = ApiGatewayProxyResponse::default(); + response.status_code = 200; + response.multi_value_headers = headers.clone(); + response.is_base64_encoded = false; + response.body = Some("Hello world!".into()); + response.headers = headers; + #[cfg(feature = "catch-all-fields")] + { + response.other = Default::default(); + } + response }; Ok(resp) } diff --git a/lambda-integration-tests/src/tenant_id_test.rs b/lambda-integration-tests/src/tenant_id_test.rs new file mode 100644 index 000000000..aeebc1827 --- /dev/null +++ b/lambda-integration-tests/src/tenant_id_test.rs @@ -0,0 +1,26 @@ +use lambda_runtime::{service_fn, Error, LambdaEvent}; +use serde_json::{json, Value}; + +async fn function_handler(event: LambdaEvent) -> Result { + let (event, context) = event.into_parts(); + + tracing::info!("Processing request with tenant ID: {:?}", context.tenant_id); + + let response = json!({ + "statusCode": 200, + "body": json!({ + "message": "Tenant ID test successful", + "request_id": context.request_id, + "tenant_id": context.tenant_id, + "input": event + }).to_string() + }); + + Ok(response) +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_runtime::tracing::init_default_subscriber(); + lambda_runtime::run(service_fn(function_handler)).await +} diff --git a/lambda-integration-tests/template.yaml b/lambda-integration-tests/template.yaml index 1aa69fc8a..9ba694e32 100644 --- a/lambda-integration-tests/template.yaml +++ b/lambda-integration-tests/template.yaml @@ -41,6 +41,18 @@ Resources: Path: /hello Method: get + TenantIdTestFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: rust-cargolambda + BuildProperties: + Binary: tenant-id-test + Properties: + CodeUri: ./ + Handler: bootstrap + Runtime: provided.al2023 + Role: !Ref LambdaRole + AuthorizerFunction: Type: AWS::Serverless::Function Metadata: @@ -59,4 +71,7 @@ Resources: Outputs: HelloApiEndpoint: Description: "API Gateway endpoint URL for HelloWorld" - Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/" \ No newline at end of file + Value: !Sub "https://${API}.execute-api.${AWS::Region}.amazonaws.com/integ-test/hello/" + TenantIdTestFunction: + Description: "Tenant ID test function name" + Value: !Ref TenantIdTestFunction \ No newline at end of file diff --git a/lambda-integration-tests/tests/tenant_id_prod_test.rs b/lambda-integration-tests/tests/tenant_id_prod_test.rs new file mode 100644 index 000000000..19c4e7c88 --- /dev/null +++ b/lambda-integration-tests/tests/tenant_id_prod_test.rs @@ -0,0 +1,57 @@ +use serde_json::json; + +#[test] +fn test_tenant_id_functionality_in_production() { + let function_name = + std::env::var("TENANT_ID_TEST_FUNCTION").expect("TENANT_ID_TEST_FUNCTION environment variable not set"); + + // Test with tenant ID + let payload_with_tenant = json!({ + "test": "tenant_id_test", + "message": "Testing with tenant ID" + }); + + let output = std::process::Command::new("aws") + .args([ + "lambda", + "invoke", + "--function-name", + &function_name, + "--payload", + &payload_with_tenant.to_string(), + "--cli-binary-format", + "raw-in-base64-out", + "/tmp/tenant_response.json", + ]) + .output() + .expect("Failed to invoke Lambda function"); + + assert!( + output.status.success(), + "Lambda invocation failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Read and verify response + let response = std::fs::read_to_string("/tmp/tenant_response.json").expect("Failed to read response file"); + + let response_json: serde_json::Value = serde_json::from_str(&response).expect("Failed to parse response JSON"); + + // Verify the function executed successfully + assert_eq!(response_json["statusCode"], 200); + + // Parse the body to check tenant_id field exists (even if null) + let body: serde_json::Value = + serde_json::from_str(response_json["body"].as_str().expect("Body should be a string")) + .expect("Failed to parse body JSON"); + + assert!( + body.get("tenant_id").is_some(), + "tenant_id field should be present in response" + ); + assert!(body.get("request_id").is_some(), "request_id should be present"); + assert_eq!(body["message"], "Tenant ID test successful"); + + println!("✅ Tenant ID production test passed"); + println!("Response: {}", serde_json::to_string_pretty(&response_json).unwrap()); +} diff --git a/lambda-runtime-api-client/CHANGELOG.md b/lambda-runtime-api-client/CHANGELOG.md new file mode 100644 index 000000000..f912e1c8d --- /dev/null +++ b/lambda-runtime-api-client/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.1.0...lambda_runtime_api_client-v1.1.1) - 2026-09-02 + +### Other + +- release ([#1151](https://github.com/aws/aws-lambda-rust-runtime/pull/1151)) + +## [1.1.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.0.3...lambda_runtime_api_client-v1.1.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +### Added + +- `PooledClient`: a Runtime API client that can rebuild its connection pool after a + SnapStart VM restore via `reset_pool()`, so stale connections to the Runtime API + (RAPID) are discarded. Implemented lock-free with `OnceLock`; `call()` has no + hot-path lock. Construct via `PooledClient::builder().build()`. +- `PooledClientBuilder`, returned by `PooledClient::builder()`. +- `RuntimeApiClient` trait abstracting `call`, implemented by both `Client` and + `PooledClient`. + +### Deprecated + +- `Client` and `ClientBuilder::build` are superseded by `PooledClient` / + `PooledClient::builder().build()`. `Client` is unchanged and fully functional; it + simply cannot reset its connection pool after a SnapStart restore. They are + documented as superseded but do **not** carry a `#[deprecated]` attribute, so no + compiler warnings are emitted; they are expected to be removed in the next major + version (2.0.0). This keeps the crate non-breaking (no major version bump). + +## [1.0.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime_api_client-v1.0.2...lambda_runtime_api_client-v1.0.3) - 2026-03-19 + +### Other + +- release ([#1118](https://github.com/aws/aws-lambda-rust-runtime/pull/1118)) + +## [1.0.2](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime_api_client-v1.0.2) - 2026-01-06 + +### Added + +- *(lambda-managed-instances)* API client connection pooling for concurrent requests ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) diff --git a/lambda-runtime-api-client/Cargo.toml b/lambda-runtime-api-client/Cargo.toml index 57fc4bca5..d1fb622c9 100644 --- a/lambda-runtime-api-client/Cargo.toml +++ b/lambda-runtime-api-client/Cargo.toml @@ -1,14 +1,15 @@ [package] name = "lambda_runtime_api_client" -version = "0.11.1" +version = "1.1.1" edition = "2021" +rust-version = "1.84.0" authors = [ "David Calavera ", "Harold Sun ", ] description = "AWS Lambda Runtime interaction API" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "README.md" @@ -32,7 +33,8 @@ hyper-util = { workspace = true, features = [ "tokio", ] } tower = { workspace = true, features = ["util"] } -tower-service = { workspace = true } -tokio = { version = "1.0", features = ["io-util"] } tracing = { version = "0.1", features = ["log"], optional = true } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "json", "env-filter"], optional = true } + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-runtime-api-client/src/body/channel.rs b/lambda-runtime-api-client/src/body/channel.rs index 275746556..f1e094c3c 100644 --- a/lambda-runtime-api-client/src/body/channel.rs +++ b/lambda-runtime-api-client/src/body/channel.rs @@ -1,5 +1,5 @@ //! Body::channel utilities. Extracted from Hyper under MIT license. -//! https://github.com/hyperium/hyper/blob/master/LICENSE +//! use std::{ pin::Pin, @@ -31,7 +31,7 @@ impl DecodedLength { } } - /// Converts to an Option representing a Known or Unknown length. + /// Converts to an `Option` representing a Known or Unknown length. pub(crate) fn into_opt(self) -> Option { match self { DecodedLength::CHUNKED | DecodedLength::CLOSE_DELIMITED => None, diff --git a/lambda-runtime-api-client/src/body/mod.rs b/lambda-runtime-api-client/src/body/mod.rs index 467356829..13bfcaa03 100644 --- a/lambda-runtime-api-client/src/body/mod.rs +++ b/lambda-runtime-api-client/src/body/mod.rs @@ -1,5 +1,5 @@ //! HTTP body utilities. Extracted from Axum under MIT license. -//! https://github.com/tokio-rs/axum/blob/main/axum/LICENSE +//! use crate::{BoxError, Error}; use bytes::Bytes; diff --git a/lambda-runtime-api-client/src/body/sender.rs b/lambda-runtime-api-client/src/body/sender.rs index 0e008454c..14c1d918e 100644 --- a/lambda-runtime-api-client/src/body/sender.rs +++ b/lambda-runtime-api-client/src/body/sender.rs @@ -1,5 +1,5 @@ //! Body::channel utilities. Extracted from Hyper under MIT license. -//! https://github.com/hyperium/hyper/blob/master/LICENSE +//! use crate::Error; use std::task::{Context, Poll}; diff --git a/lambda-runtime-api-client/src/body/watch.rs b/lambda-runtime-api-client/src/body/watch.rs index a5f8ae412..f31f4f273 100644 --- a/lambda-runtime-api-client/src/body/watch.rs +++ b/lambda-runtime-api-client/src/body/watch.rs @@ -1,5 +1,5 @@ //! Body::channel utilities. Extracted from Hyper under MIT license. -//! https://github.com/hyperium/hyper/blob/master/LICENSE +//! //! An SPSC broadcast channel. //! diff --git a/lambda-runtime-api-client/src/error.rs b/lambda-runtime-api-client/src/error.rs index dbb87b642..d8ff30b22 100644 --- a/lambda-runtime-api-client/src/error.rs +++ b/lambda-runtime-api-client/src/error.rs @@ -1,5 +1,5 @@ //! Extracted from Axum under MIT license. -//! https://github.com/tokio-rs/axum/blob/main/axum/LICENSE +//! use std::{error::Error as StdError, fmt}; pub use tower::BoxError; /// Errors that can happen when using axum. diff --git a/lambda-runtime-api-client/src/lib.rs b/lambda-runtime-api-client/src/lib.rs index 00c00e4c9..ead8abe75 100644 --- a/lambda-runtime-api-client/src/lib.rs +++ b/lambda-runtime-api-client/src/lib.rs @@ -1,6 +1,7 @@ #![deny(clippy::all, clippy::cargo)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] #![allow(clippy::multiple_crate_versions)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! This crate includes a base HTTP client to interact with //! the AWS Lambda Runtime API. @@ -11,7 +12,7 @@ use http::{ }; use hyper::body::Incoming; use hyper_util::client::legacy::connect::HttpConnector; -use std::{convert::TryInto, fmt::Debug, future}; +use std::{convert::TryInto, fmt::Debug, future, sync::OnceLock}; const USER_AGENT_HEADER: &str = "User-Agent"; const DEFAULT_USER_AGENT: &str = concat!("aws-lambda-rust/", env!("CARGO_PKG_VERSION")); @@ -22,9 +23,23 @@ pub use error::*; pub mod body; #[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] pub mod tracing; +/// The single method the runtime needs from any client: send a request to the +/// Lambda Runtime API (RAPID). Implemented by both [`Client`] and [`PooledClient`]. +pub trait RuntimeApiClient { + /// Send a given request to the Runtime API. + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>>; +} + /// API client to interact with the AWS Lambda Runtime API. +/// +/// **Superseded by [`PooledClient`]**, which additionally supports rebuilding its +/// connection pool after a SnapStart VM restore via [`PooledClient::reset_pool`]. +/// `Client` remains fully functional; it simply cannot reset its pool after a +/// restore. Prefer [`PooledClient`] for new code; `Client` is expected to be +/// removed in the next major version (2.0.0). #[derive(Debug)] pub struct Client { /// The runtime API URI @@ -36,70 +51,167 @@ pub struct Client { impl Client { /// Create a builder struct to configure the client. pub fn builder() -> ClientBuilder { - ClientBuilder { - connector: HttpConnector::new(), - uri: None, - } + ClientBuilder::new() } -} -impl Client { /// Send a given request to the Runtime API. /// Use the client's base URI to ensure the API endpoint is correct. pub fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + ::call(self, req) + } + + /// Create a new client with a given base URI, HTTP connector, and optional pool size hint. + fn with(base: Uri, connector: HttpConnector, pool_size: Option) -> Self { + Self { + base, + client: build_hyper_client(&connector, pool_size), + } + } +} + +impl RuntimeApiClient for Client { + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { // NOTE: This method returns a boxed future such that the future has a static lifetime. // Due to limitations around the Rust async implementation as of Mar 2024, this is // required to minimize constraints on the handler passed to [lambda_runtime::run]. - let req = match self.set_origin(req) { + let req = match set_origin(&self.base, req) { Ok(req) => req, Err(err) => return future::ready(Err(err)).boxed(), }; self.client.request(req).map_err(Into::into).boxed() } +} + +/// API client to interact with the AWS Lambda Runtime API, with support for +/// rebuilding its connection pool after a SnapStart VM restore. +#[derive(Debug)] +pub struct PooledClient { + /// The runtime API URI + pub base: Uri, + /// The client that manages the API connections + client: hyper_util::client::legacy::Client, + /// The HTTP connector used to rebuild the client after a SnapStart restore + connector: HttpConnector, + /// Optional pool size hint for the hyper client + pool_size: Option, + /// Holds a freshly built client after a SnapStart restore. When set, [`PooledClient::call`] + /// uses it instead of [`PooledClient::client`]; otherwise the original client is used. + /// + /// This is populated exactly once by [`PooledClient::reset_pool`] during the restore + /// lifecycle, before any concurrent polling starts, so the `OnceLock` write never + /// races with reads. Using `OnceLock` keeps `call` lock-free on the hot path. + restored: OnceLock>, +} + +impl PooledClient { + /// Create a builder struct to configure the client. + pub fn builder() -> PooledClientBuilder { + PooledClientBuilder::new() + } + + /// Send a given request to the Runtime API. + /// Use the client's base URI to ensure the API endpoint is correct. + pub fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + ::call(self, req) + } - /// Create a new client with a given base URI and HTTP connector. - fn with(base: Uri, connector: HttpConnector) -> Self { - let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()) - .http1_max_buf_size(1024 * 1024) - .build(connector); - Self { base, client } + /// Create a new client with a given base URI, HTTP connector, and optional pool size hint. + fn with(base: Uri, connector: HttpConnector, pool_size: Option) -> Self { + let client = build_hyper_client(&connector, pool_size); + Self { + base, + client, + connector, + pool_size, + restored: OnceLock::new(), + } } - fn set_origin(&self, req: Request) -> Result, BoxError> { - let (mut parts, body) = req.into_parts(); - let (scheme, authority, base_path) = { - let scheme = self.base.scheme().unwrap_or(&Scheme::HTTP); - let authority = self.base.authority().expect("Authority not found"); - let base_path = self.base.path().trim_end_matches('/'); - (scheme, authority, base_path) + /// Reset the internal connection pool by building a fresh hyper client that + /// subsequent calls will use. + /// + /// This is useful after a SnapStart VM restore where existing connections + /// to the Lambda Runtime API (RAPID) are stale and must be discarded. It is + /// called exactly once during the restore lifecycle, before concurrent + /// polling starts, so the underlying `OnceLock` write never races. + pub fn reset_pool(&self) { + let _ = self.restored.set(build_hyper_client(&self.connector, self.pool_size)); + } +} + +impl RuntimeApiClient for PooledClient { + fn call(&self, req: Request) -> BoxFuture<'static, Result, BoxError>> { + let req = match set_origin(&self.base, req) { + Ok(req) => req, + Err(err) => return future::ready(Err(err)).boxed(), }; - let path = parts.uri.path_and_query().expect("PathAndQuery not found"); - let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid"); + // After a SnapStart restore, `restored` holds a fresh client with no stale + // connections; otherwise fall back to the original client. Lock-free read. + let hyper_client = self.restored.get().unwrap_or(&self.client); + hyper_client.request(req).map_err(Into::into).boxed() + } +} - let uri = Uri::builder() - .scheme(scheme.as_ref()) - .authority(authority.as_ref()) - .path_and_query(pq) - .build() - .map_err(Box::new)?; +/// Build a fresh hyper client from the given connector and pool size settings. +fn build_hyper_client( + connector: &HttpConnector, + pool_size: Option, +) -> hyper_util::client::legacy::Client { + let mut builder = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new()); + builder.http1_max_buf_size(1024 * 1024); - parts.uri = uri; - Ok(Request::from_parts(parts, body)) + if let Some(size) = pool_size { + builder.pool_max_idle_per_host(size); } + + builder.build(connector.clone()) +} + +/// Rewrite a request's URI against the client's base (scheme/authority/base path). +fn set_origin(base: &Uri, req: Request) -> Result, BoxError> { + let (mut parts, body) = req.into_parts(); + let (scheme, authority, base_path) = { + let scheme = base.scheme().unwrap_or(&Scheme::HTTP); + let authority = base.authority().expect("Authority not found"); + let base_path = base.path().trim_end_matches('/'); + (scheme, authority, base_path) + }; + let path = parts.uri.path_and_query().expect("PathAndQuery not found"); + let pq: PathAndQuery = format!("{base_path}{path}").parse().expect("PathAndQuery invalid"); + + let uri = Uri::builder() + .scheme(scheme.as_ref()) + .authority(authority.as_ref()) + .path_and_query(pq) + .build() + .map_err(Box::new)?; + + parts.uri = uri; + Ok(Request::from_parts(parts, body)) } /// Builder implementation to construct any Runtime API clients. pub struct ClientBuilder { connector: HttpConnector, uri: Option, + pool_size: Option, } impl ClientBuilder { + fn new() -> ClientBuilder { + ClientBuilder { + connector: HttpConnector::new(), + uri: None, + pool_size: None, + } + } + /// Create a new builder with a given HTTP connector. pub fn with_connector(self, connector: HttpConnector) -> ClientBuilder { ClientBuilder { connector, uri: self.uri, + pool_size: self.pool_size, } } @@ -109,16 +221,83 @@ impl ClientBuilder { Self { uri: Some(uri), ..self } } + /// Provide a pool size hint for the underlying Hyper client. + /// + /// When using concurrent polling, this should be at least the maximum + /// concurrency (e.g., `AWS_LAMBDA_MAX_CONCURRENCY`) to avoid connection + /// starvation. + pub fn with_pool_size(self, pool_size: usize) -> Self { + Self { + pool_size: Some(pool_size), + ..self + } + } + /// Create the new client to interact with the Runtime API. + /// + /// **Superseded by [`PooledClient::builder`]**, which produces a + /// [`PooledClient`] that supports SnapStart connection-pool reset. Prefer + /// `PooledClient::builder().build()` for new code; this method is expected to + /// be removed in the next major version (2.0.0). pub fn build(self) -> Result { - let uri = match self.uri { - Some(uri) => uri, - None => { - let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var"); - uri.try_into().expect("Unable to convert to URL") - } - }; - Ok(Client::with(uri, self.connector)) + let uri = resolve_uri(self.uri); + Ok(Client::with(uri, self.connector, self.pool_size)) + } +} + +/// Builder to construct a [`PooledClient`]. +pub struct PooledClientBuilder { + connector: HttpConnector, + uri: Option, + pool_size: Option, +} + +impl PooledClientBuilder { + fn new() -> PooledClientBuilder { + PooledClientBuilder { + connector: HttpConnector::new(), + uri: None, + pool_size: None, + } + } + + /// Use a given HTTP connector. + pub fn with_connector(self, connector: HttpConnector) -> PooledClientBuilder { + PooledClientBuilder { connector, ..self } + } + + /// Use a given base URI. Inherits all other attributes from the existing builder. + pub fn with_endpoint(self, uri: http::Uri) -> Self { + Self { uri: Some(uri), ..self } + } + + /// Provide a pool size hint for the underlying Hyper client. + /// + /// When using concurrent polling, this should be at least the maximum + /// concurrency (e.g., `AWS_LAMBDA_MAX_CONCURRENCY`) to avoid connection + /// starvation. + pub fn with_pool_size(self, pool_size: usize) -> Self { + Self { + pool_size: Some(pool_size), + ..self + } + } + + /// Create the new [`PooledClient`] to interact with the Runtime API. + pub fn build(self) -> PooledClient { + let uri = resolve_uri(self.uri); + PooledClient::with(uri, self.connector, self.pool_size) + } +} + +/// Resolve the base URI from an explicit value or the `AWS_LAMBDA_RUNTIME_API` env var. +fn resolve_uri(uri: Option) -> Uri { + match uri { + Some(uri) => uri, + None => { + let uri = std::env::var("AWS_LAMBDA_RUNTIME_API").expect("Missing AWS_LAMBDA_RUNTIME_API env var"); + uri.try_into().expect("Unable to convert to URL") + } } } @@ -141,13 +320,12 @@ mod tests { #[test] fn test_set_origin() { - let base = "http://localhost:9001"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/2018-06-01/runtime/invocation/next", &req.uri().to_string() @@ -156,28 +334,49 @@ mod tests { #[test] fn test_set_origin_with_base_path() { - let base = "http://localhost:9001/foo"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001/foo".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/foo/2018-06-01/runtime/invocation/next", &req.uri().to_string() ); - let base = "http://localhost:9001/foo/"; - let client = Client::builder().with_endpoint(base.parse().unwrap()).build().unwrap(); + let base: Uri = "http://localhost:9001/foo/".parse().unwrap(); let req = build_request() .uri("/2018-06-01/runtime/invocation/next") .body(()) .unwrap(); - let req = client.set_origin(req).unwrap(); + let req = set_origin(&base, req).unwrap(); assert_eq!( "http://localhost:9001/foo/2018-06-01/runtime/invocation/next", &req.uri().to_string() ); } + + #[test] + fn builder_accepts_pool_size() { + let base = "http://localhost:9001"; + let expected: Uri = base.parse().unwrap(); + let client = PooledClient::builder() + .with_pool_size(4) + .with_endpoint(base.parse().unwrap()) + .build(); + + assert_eq!(client.base, expected); + } + + #[test] + fn test_reset_pool() { + let base = "http://localhost:9001"; + let client = PooledClient::builder() + .with_pool_size(4) + .with_endpoint(base.parse().unwrap()) + .build(); + client.reset_pool(); + assert_eq!(client.base, base.parse::().unwrap()); + } } diff --git a/lambda-runtime-api-client/src/tracing.rs b/lambda-runtime-api-client/src/tracing.rs index 09f41991f..5aa9bfa17 100644 --- a/lambda-runtime-api-client/src/tracing.rs +++ b/lambda-runtime-api-client/src/tracing.rs @@ -14,12 +14,16 @@ pub use tracing::*; /// Re-export the `tracing-subscriber` crate to build your own subscribers. pub use tracing_subscriber as subscriber; +use tracing_subscriber::fmt::MakeWriter; const DEFAULT_LOG_LEVEL: &str = "INFO"; /// Initialize `tracing-subscriber` with default logging options. /// -/// This function uses environment variables set with [Lambda's advance logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) +/// The default subscriber writes logs to STDOUT in the current context. +/// If you want to customize the writer, see [`init_default_subscriber_with_writer()`]. +/// +/// This function uses environment variables set with [Lambda's advanced logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) /// if they're configured for your function. /// /// This subscriber sets the logging level based on environment variables: @@ -31,6 +35,32 @@ const DEFAULT_LOG_LEVEL: &str = "INFO"; /// If the `AWS_LAMBDA_LOG_FORMAT` environment variable is set to `JSON`, the log lines will be formatted as json objects, /// otherwise they will be formatted with the default tracing format. pub fn init_default_subscriber() { + init_default_subscriber_with_writer(std::io::stdout); +} + +/// Initialize `tracing-subscriber` with default logging options, and a custom writer. +/// +/// You might want to avoid writing to STDOUT in the local context via [`init_default_subscriber()`], if you have a high-throughput Lambdas that involve +/// a lot of async concurrency. Since, writing to STDOUT can briefly block your tokio runtime - ref [tracing #2653](https://github.com/tokio-rs/tracing/issues/2653). +/// In that case, you might prefer to use [tracing_appender::NonBlocking](https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.NonBlocking.html) instead - particularly if your Lambda is fairly long-running and stays warm. +/// Though, note that you are then responsible +/// for ensuring gracefuls shutdown. See [aws-samples/graceful-shutdown-with-aws-lambda](https://github.com/aws-samples/graceful-shutdown-with-aws-lambda) for a complete example. +/// +/// This function uses environment variables set with [Lambda's advanced logging controls](https://aws.amazon.com/blogs/compute/introducing-advanced-logging-controls-for-aws-lambda-functions/) +/// if they're configured for your function. +/// +/// This subscriber sets the logging level based on environment variables: +/// - if `AWS_LAMBDA_LOG_LEVEL` is set, it takes precedence over any other environment variables. +/// - if `AWS_LAMBDA_LOG_LEVEL` is not set, check if `RUST_LOG` is set. +/// - if none of those two variables are set, use `INFO` as the logging level. +/// +/// The logging format can also be changed based on Lambda's advanced logging controls. +/// If the `AWS_LAMBDA_LOG_FORMAT` environment variable is set to `JSON`, the log lines will be formatted as json objects, +/// otherwise they will be formatted with the default tracing format. +pub fn init_default_subscriber_with_writer(writer: Writer) +where + Writer: for<'writer> MakeWriter<'writer> + Send + Sync + 'static, +{ let log_format = env::var("AWS_LAMBDA_LOG_FORMAT").unwrap_or_default(); let log_level_str = env::var("AWS_LAMBDA_LOG_LEVEL").or_else(|_| env::var("RUST_LOG")); let log_level = @@ -43,7 +73,8 @@ pub fn init_default_subscriber() { EnvFilter::builder() .with_default_directive(log_level.into()) .from_env_lossy(), - ); + ) + .with_writer(writer); if log_format.eq_ignore_ascii_case("json") { collector.json().init() diff --git a/lambda-runtime/CHANGELOG.md b/lambda-runtime/CHANGELOG.md new file mode 100644 index 000000000..b06473406 --- /dev/null +++ b/lambda-runtime/CHANGELOG.md @@ -0,0 +1,128 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.5.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.4.0...lambda_runtime-v1.5.0) - 2026-09-29 + +### Added + +- log worker count and max concurrency ([#1167](https://github.com/aws/aws-lambda-rust-runtime/pull/1167)) + +### Other + +- explain AWS-LC cold-start tradeoff ([#1172](https://github.com/aws/aws-lambda-rust-runtime/pull/1172)) +- Add #[serde(default)] to `to` field of SimpleEmailCommonHeaders ([#1171](https://github.com/aws/aws-lambda-rust-runtime/pull/1171)) + +## [1.4.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.3.0...lambda_runtime-v1.4.0) - 2026-09-02 + +### Added + +- add lambda-runtime-invocation-id header ([#1159](https://github.com/aws/aws-lambda-rust-runtime/pull/1159)) + +### Other + +- Record faas.name on Lambda OTEL spans. ([#1163](https://github.com/aws/aws-lambda-rust-runtime/pull/1163)) +- show custom event handler usage ([#1154](https://github.com/aws/aws-lambda-rust-runtime/pull/1154)) +- release ([#1151](https://github.com/aws/aws-lambda-rust-runtime/pull/1151)) + +## [1.3.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.2.1...lambda_runtime-v1.3.0) - 2026-07-09 + +### Added + +- add SnapStart support via SnapStartResource trait ([#1150](https://github.com/aws/aws-lambda-rust-runtime/pull/1150)) + +## [1.2.1](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.2.0...lambda_runtime-v1.2.1) - 2026-05-25 + +### Other + +- *(deps)* update opentelemetry-semantic-conventions requirement ([#1147](https://github.com/aws/aws-lambda-rust-runtime/pull/1147)) + +## [1.2.0](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.3...lambda_runtime-v1.2.0) - 2026-05-08 + +### Added + +- Add VPC Lattice as an upstream source for lambda-http ([#1136](https://github.com/aws/aws-lambda-rust-runtime/pull/1136)) + +### Fixed + +- *(lambda-http)* into_api_gateway_v2_request joins cookies with "; " ([#1143](https://github.com/aws/aws-lambda-rust-runtime/pull/1143)) + +### Other + +- Added `builders` pass-through feature to enable `aws_lambda_events/builders` ([#1146](https://github.com/aws/aws-lambda-rust-runtime/pull/1146)) + +## [1.1.3](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.2...lambda_runtime-v1.1.3) - 2026-04-16 + +### Other + +- remove non exhaustive from time structs ([#1138](https://github.com/aws/aws-lambda-rust-runtime/pull/1138)) + +## [1.1.2](https://github.com/aws/aws-lambda-rust-runtime/compare/lambda_runtime-v1.1.1...lambda_runtime-v1.1.2) - 2026-03-19 + +### Fixed + +- fix handling of null groupConfiguration ([#1130](https://github.com/aws/aws-lambda-rust-runtime/pull/1130)) + +### Other + +- release ([#1118](https://github.com/aws/aws-lambda-rust-runtime/pull/1118)) + +## [1.1.1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime-v1.1.1) - 2026-03-11 + +Thank you to all the contributors who helped make this release possible. We appreciate your time, effort, and passion for the Rust Lambda community. ❤️ + +### Lambda Managed Instances + +The runtime now supports Lambda Managed Instances via the `concurrency-tokio` feature flag (previously `experimental-concurrency`). Lambda Managed Instances allow multiple concurrent requests to be processed within a single execution environment. When `AWS_LAMBDA_MAX_CONCURRENCY` is set, the runtime spawns multiple independent long-poll workers to handle concurrent invocations. If the env var is unset or <= 1, it falls back to sequential behavior automatically — so the same handler works on both classic Lambda and Lambda Managed Instances. ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +```toml +[dependencies] +lambda_runtime = { version = "1.1", features = ["concurrency-tokio"] } +``` + +```rust +lambda_runtime::run_concurrent(service_fn(my_handler)).await?; +``` + +For a complete working example, see [examples/basic-lambda-concurrent](https://github.com/aws/aws-lambda-rust-runtime/tree/main/examples/basic-lambda-concurrent). For detailed guidance on building functions for multi-concurrency, including shared state patterns and database connection pools, see the [Rust runtime for Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-rust.html) documentation. + +We would like also to involve the community in a broader discussion about improving our approach on multiconcurrency. You can find the discussion in ([#1120](https://github.com/aws/aws-lambda-rust-runtime/issues/1120)) + +### Added + +- *(lambda-managed-instances)* log non-2xx Lambda Runtime API responses with status and body ([#1109](https://github.com/aws/aws-lambda-rust-runtime/pull/1109)) +- tenant ID propagation for multi-tenant Lambda use cases. The `tenant_id` is available in the `context.tenant_id` field (`Option`) and is automatically extracted from the `lambda-runtime-aws-tenant-id` header when present ([#1082](https://github.com/aws/aws-lambda-rust-runtime/pull/1082)) +- Add builder pattern support for event response types ([#1090](https://github.com/aws/aws-lambda-rust-runtime/pull/1090)) + +### Fixed + +- *(test)* fix test_concurrent_structured_logging_isolation ([#1121](https://github.com/aws/aws-lambda-rust-runtime/pull/1121)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) +- X-Ray trace ID now sourced from `Context` instead of environment variables ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) + +### Other + +- *(lambda-managed-instances)* add `tokio_unstable` to known cfgs to avoid linter warns ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- *(lambda-managed-instances)* verify request-ID isolation in concurrent exec ([#1086](https://github.com/aws/aws-lambda-rust-runtime/pull/1086)) +- *(lambda-managed-instances)* warn on `run()` with `AWS_LAMBDA_MAX_CONCURRENCY`, rename feature `experimental-concurrency` to `concurrency-tokio` ([#1095](https://github.com/aws/aws-lambda-rust-runtime/pull/1095)) +- Introducing Harness Testing ([#1103](https://github.com/aws/aws-lambda-rust-runtime/pull/1103)) + +## [1.1.0-rc1](https://github.com/aws/aws-lambda-rust-runtime/compare/v1.0.2...lambda_runtime-v1.1.0-rc1) - 2026-02-04 + +### Added + +- *(lambda-managed-instances)* Lambda Managed Instances support via `concurrency-tokio` feature flag ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) +- *(lambda-managed-instances)* tenant ID propagation for multi-tenant Lambda use cases ([#1082](https://github.com/aws/aws-lambda-rust-runtime/pull/1082)) + +### Changed + +- MSRV updated from 1.82.0 to 1.84.0, enabled MSRV-aware resolver ([#1078](https://github.com/aws/aws-lambda-rust-runtime/pull/1078)) +- X-Ray trace ID now sourced from `Context` instead of environment variables ([#1067](https://github.com/aws/aws-lambda-rust-runtime/pull/1067)) diff --git a/lambda-runtime/Cargo.toml b/lambda-runtime/Cargo.toml index b4a7ad3d4..b4876663b 100644 --- a/lambda-runtime/Cargo.toml +++ b/lambda-runtime/Cargo.toml @@ -1,18 +1,22 @@ [package] name = "lambda_runtime" -version = "0.13.0" +version = "1.5.0" authors = [ "David Calavera ", "Harold Sun ", ] description = "AWS Lambda Runtime" edition = "2021" +rust-version = "1.84.0" license = "Apache-2.0" -repository = "https://github.com/awslabs/aws-lambda-rust-runtime" +repository = "https://github.com/aws/aws-lambda-rust-runtime" categories = ["web-programming::http-server"] keywords = ["AWS", "Lambda", "API"] readme = "../README.md" +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } + [features] default = ["tracing"] tracing = ["lambda_runtime_api_client/tracing"] # enables access to the Tracing utilities @@ -20,6 +24,11 @@ opentelemetry = ["opentelemetry-semantic-conventions"] # enables access to the O anyhow = ["dep:anyhow"] # enables From for Diagnostic for anyhow error types, see README.md for more info eyre = ["dep:eyre"] # enables From for Diagnostic for eyre error types, see README.md for more info miette = ["dep:miette"] # enables From for Diagnostic for miette error types, see README.md for more info +# TODO: remove tokio/rt and rt-multi-thread from non-feature-flagged dependencies in new breaking version, since they are unused: +# as well as default features +# https://github.com/aws/aws-lambda-rust-runtime/issues/984 +graceful-shutdown = ["tokio/rt", "tokio/signal", "dep:lambda-extension"] +concurrency-tokio = [] [dependencies] anyhow = { version = "1.0.86", optional = true } @@ -29,24 +38,18 @@ bytes = { workspace = true } eyre = { version = "0.6.12", optional = true } futures = { workspace = true } http = { workspace = true } -http-body = { workspace = true } http-body-util = { workspace = true } http-serde = { workspace = true } hyper = { workspace = true, features = ["http1", "client"] } -hyper-util = { workspace = true, features = [ - "client", - "client-legacy", - "http1", - "tokio", -] } -lambda_runtime_api_client = { version = "0.11.1", path = "../lambda-runtime-api-client", default-features = false } +lambda-extension = { version = "1.1", path = "../lambda-extension", default-features = false, optional = true } +lambda_runtime_api_client = { version = "1.1.1", path = "../lambda-runtime-api-client", default-features = false } miette = { version = "7.2.0", optional = true } -opentelemetry-semantic-conventions = { version = "0.27", optional = true, features = ["semconv_experimental"] } +opentelemetry-semantic-conventions = { version = "0.32", optional = true, features = ["semconv_experimental"] } pin-project = "1" serde = { version = "1", features = ["derive", "rc"] } serde_json = "^1" serde_path_to_error = "0.1.11" -tokio = { version = "1.0", features = [ +tokio = { version = "1.46", features = [ "macros", "io-util", "sync", @@ -54,11 +57,10 @@ tokio = { version = "1.0", features = [ ] } tokio-stream = "0.1.2" tower = { workspace = true, features = ["util"] } -tower-layer = { workspace = true } tracing = { version = "0.1", features = ["log"] } [dev-dependencies] -httpmock = "0.7.0" +httpmock = "0.8.1" hyper-util = { workspace = true, features = [ "client", "client-legacy", @@ -67,4 +69,16 @@ hyper-util = { workspace = true, features = [ "server-auto", "tokio", ] } +# pin back to pre-1.2.1 to avoid breaking rust MSRV of 1.81: +# https://github.com/hsivonen/idna_adapter/commit/f948802e3a2ae936eec51886eefbd7d536a28791 +idna_adapter = "=1.2.0" +# Self dependency to enable the graceful-shutdown feature for tests +lambda_runtime = { path = ".", features = ["tracing", "graceful-shutdown"] } pin-project-lite = { workspace = true } +tracing-appender = "0.2" +tracing-capture = "0.1.0" +tracing-subscriber = { version = "0.3", features = ["registry"] } +tracing-test = "0.2" + +[package.metadata.docs.rs] +all-features = true diff --git a/lambda-runtime/src/constants.rs b/lambda-runtime/src/constants.rs new file mode 100644 index 000000000..98c789d0f --- /dev/null +++ b/lambda-runtime/src/constants.rs @@ -0,0 +1,9 @@ +/// Header names used in the Lambda Runtime API. +pub(crate) const LAMBDA_RUNTIME_REQUEST_ID: &str = "lambda-runtime-aws-request-id"; +pub(crate) const LAMBDA_RUNTIME_DEADLINE_MS: &str = "lambda-runtime-deadline-ms"; +pub(crate) const LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN: &str = "lambda-runtime-invoked-function-arn"; +pub(crate) const LAMBDA_RUNTIME_TRACE_ID: &str = "lambda-runtime-trace-id"; +pub(crate) const LAMBDA_RUNTIME_CLIENT_CONTEXT: &str = "lambda-runtime-client-context"; +pub(crate) const LAMBDA_RUNTIME_COGNITO_IDENTITY: &str = "lambda-runtime-cognito-identity"; +pub(crate) const LAMBDA_RUNTIME_TENANT_ID: &str = "lambda-runtime-aws-tenant-id"; +pub(crate) const LAMBDA_RUNTIME_INVOCATION_ID: &str = "lambda-runtime-invocation-id"; diff --git a/lambda-runtime/src/diagnostic.rs b/lambda-runtime/src/diagnostic.rs index c03ce284b..60917e317 100644 --- a/lambda-runtime/src/diagnostic.rs +++ b/lambda-runtime/src/diagnostic.rs @@ -119,6 +119,7 @@ impl From for Diagnostic { } #[cfg(feature = "anyhow")] +#[cfg_attr(docsrs, doc(cfg(feature = "anyhow")))] impl From for Diagnostic { fn from(value: anyhow::Error) -> Diagnostic { Diagnostic { @@ -129,6 +130,7 @@ impl From for Diagnostic { } #[cfg(feature = "eyre")] +#[cfg_attr(docsrs, doc(cfg(feature = "eyre")))] impl From for Diagnostic { fn from(value: eyre::Report) -> Diagnostic { Diagnostic { @@ -139,6 +141,7 @@ impl From for Diagnostic { } #[cfg(feature = "miette")] +#[cfg_attr(docsrs, doc(cfg(feature = "miette")))] impl From for Diagnostic { fn from(value: miette::Report) -> Diagnostic { Diagnostic { diff --git a/lambda-runtime/src/layers/api_client.rs b/lambda-runtime/src/layers/api_client.rs index d44a84f25..d91ea78f7 100644 --- a/lambda-runtime/src/layers/api_client.rs +++ b/lambda-runtime/src/layers/api_client.rs @@ -1,7 +1,10 @@ +// `Client` is referenced only as the default for the `C` generic parameter, kept +// for backward compatibility of the public `RuntimeApiClientService`/future types. + use crate::LambdaInvocation; use futures::{future::BoxFuture, ready, FutureExt, TryFutureExt}; use hyper::body::Incoming; -use lambda_runtime_api_client::{body::Body, BoxError, Client}; +use lambda_runtime_api_client::{body::Body, BoxError, Client, RuntimeApiClient}; use pin_project::pin_project; use std::{future::Future, pin::Pin, sync::Arc, task}; use tower::Service; @@ -13,25 +16,26 @@ use tracing::error; /// This type is only meant for internal use in the Lambda runtime crate. It neither augments the /// inner service's request type nor its error type. However, this service returns an empty /// response `()` as the Lambda request has been completed. -pub struct RuntimeApiClientService { +pub struct RuntimeApiClientService { inner: S, - client: Arc, + client: Arc, } -impl RuntimeApiClientService { - pub fn new(inner: S, client: Arc) -> Self { +impl RuntimeApiClientService { + pub fn new(inner: S, client: Arc) -> Self { Self { inner, client } } } -impl Service for RuntimeApiClientService +impl Service for RuntimeApiClientService where S: Service, S::Future: Future, BoxError>>, + C: RuntimeApiClient, { type Response = (); type Error = S::Error; - type Future = RuntimeApiClientFuture; + type Future = RuntimeApiClientFuture; fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> task::Poll> { self.inner.poll_ready(cx) @@ -44,15 +48,28 @@ where } } +impl Clone for RuntimeApiClientService +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + client: self.client.clone(), + } + } +} + #[pin_project(project = RuntimeApiClientFutureProj)] -pub enum RuntimeApiClientFuture { - First(#[pin] F, Arc), +pub enum RuntimeApiClientFuture { + First(#[pin] F, Arc), Second(#[pin] BoxFuture<'static, Result, BoxError>>), } -impl Future for RuntimeApiClientFuture +impl Future for RuntimeApiClientFuture where F: Future, BoxError>>, + C: RuntimeApiClient, { type Output = Result<(), BoxError>; @@ -73,10 +90,236 @@ where .boxed(); self.set(RuntimeApiClientFuture::Second(next_fut)); } - Err(err) => break Err(err), + Err(err) => { + log_or_print!( + tracing: tracing::error!(error = ?err, "failed to build Lambda Runtime API request"), + fallback: eprintln!("failed to build Lambda Runtime API request: {err:?}") + ); + break Err(err); + } + }, + RuntimeApiClientFutureProj::Second(fut) => match ready!(fut.poll(cx)) { + Ok(resp) if !resp.status().is_success() => { + let status = resp.status(); + + // TODO + // we should consume the response body of the call in order to give a more specific message. + // https://github.com/aws/aws-lambda-rust-runtime/issues/1110 + + log_or_print!( + tracing: tracing::error!(status = %status, "Lambda Runtime API returned non-200 response"), + fallback: eprintln!("Lambda Runtime API returned non-200 response: status={status}") + ); + + // Adding more information on top of 410 Gone, to make it more clear since we cannot access the body of the message + if status == 410 { + log_or_print!( + tracing: tracing::error!( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + ), + fallback: eprintln!( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + ) + ); + } + + // Return Ok to maintain existing contract - runtime continues despite API errors + break Ok(()); + } + Ok(_) => break Ok(()), + Err(err) => { + log_or_print!( + tracing: tracing::error!(error = ?err, "Lambda Runtime API request failed"), + fallback: eprintln!("Lambda Runtime API request failed: {err:?}") + ); + break Err(err); + } }, - RuntimeApiClientFutureProj::Second(fut) => break ready!(fut.poll(cx)).map(|_| ()), } }) } } + +#[cfg(test)] +mod tests { + use super::*; + use http::StatusCode; + use http_body_util::Full; + use hyper::body::Bytes; + use lambda_runtime_api_client::body::Body; + use std::convert::Infallible; + use tokio::net::TcpListener; + use tracing_test::traced_test; + + async fn start_mock_server(status: StatusCode) -> String { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let url = format!("http://{}", addr); + + tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + let io = hyper_util::rt::TokioIo::new(stream); + + let service = hyper::service::service_fn(move |_req| async move { + Ok::<_, Infallible>( + http::Response::builder() + .status(status) + .body(Full::new(Bytes::from("test response"))) + .unwrap(), + ) + }); + + let _ = hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) + .serve_connection(io, service) + .await; + }); + + // Give the server a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + url + } + + #[tokio::test] + #[traced_test] + async fn test_successful_response() { + let url = start_mock_server(StatusCode::OK).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + assert!(result.is_ok()); + // No error logs should be present + assert!(!logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_410_timeout_error() { + let url = start_mock_server(StatusCode::GONE).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + assert!(logs_contain( + "Lambda Runtime API rejected the response: invocation timed out or response was stale" + )); + } + + #[tokio::test] + #[traced_test] + async fn test_500_error() { + let url = start_mock_server(StatusCode::INTERNAL_SERVER_ERROR).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged with status code + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_404_error() { + let url = start_mock_server(StatusCode::NOT_FOUND).await; + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint(url.parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Returns Ok to maintain contract, but logs the error + assert!(result.is_ok()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API returned non-200 response")); + } + + #[tokio::test] + #[traced_test] + async fn test_request_build_error() { + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint("http://localhost:9001".parse().unwrap()) + .build() + .unwrap(), + ); + + let request_fut = async { Err::, BoxError>("Request build error".into()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("Request build error")); + + // Verify the error was logged + assert!(logs_contain("failed to build Lambda Runtime API request")); + } + + #[tokio::test] + #[traced_test] + async fn test_network_error() { + // Use an invalid endpoint that will fail to connect + let client = Arc::new( + lambda_runtime_api_client::Client::builder() + .with_endpoint("http://127.0.0.1:1".parse().unwrap()) // Port 1 should be unreachable + .build() + .unwrap(), + ); + + let request_fut = + async { Ok::<_, BoxError>(http::Request::builder().uri("/test").body(Body::empty()).unwrap()) }; + + let future = RuntimeApiClientFuture::First(request_fut, client); + let result = future.await; + + // Network errors should propagate as Err + assert!(result.is_err()); + + // Verify the error was logged + assert!(logs_contain("Lambda Runtime API request failed")); + } +} diff --git a/lambda-runtime/src/layers/api_response.rs b/lambda-runtime/src/layers/api_response.rs index e744cde10..54a2bfb1c 100644 --- a/lambda-runtime/src/layers/api_response.rs +++ b/lambda-runtime/src/layers/api_response.rs @@ -1,5 +1,7 @@ use crate::{ + constants::LAMBDA_RUNTIME_INVOCATION_ID, deserializer, + rate_limiter::rate_limited, requests::{EventCompletionRequest, IntoRequest}, runtime::LambdaInvocation, Diagnostic, EventErrorRequest, IntoFunctionResponse, LambdaEvent, @@ -8,9 +10,11 @@ use futures::{ready, Stream}; use lambda_runtime_api_client::{body::Body, BoxError}; use pin_project::pin_project; use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, future::Future, marker::PhantomData, pin::Pin, task}; +use std::{fmt::Debug, future::Future, marker::PhantomData, pin::Pin, task, time::Duration}; use tower::Service; -use tracing::{error, trace}; +use tracing::{error, trace, warn}; + +const MALFORMED_INVOCATION_ID_LOG_INTERVAL: Duration = Duration::from_secs(60); /// Tower service that turns the result or an error of a handler function into a Lambda Runtime API /// response. @@ -51,6 +55,27 @@ impl Clone + for RuntimeApiResponseService< + S, + EventPayload, + Response, + BufferedResponse, + StreamingResponse, + StreamItem, + StreamError, + > +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _phantom: PhantomData, + } + } +} + impl Service for RuntimeApiResponseService< S, @@ -85,7 +110,7 @@ where #[cfg(debug_assertions)] if req.parts.status.is_server_error() { error!("Lambda Runtime server returned an unexpected error"); - return RuntimeApiResponseFuture::Ready(Some(Err(req.parts.status.to_string().into()))); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(req.parts.status.to_string().into())))); } // Utility closure to propagate potential error from conditionally executed trace @@ -98,33 +123,60 @@ where }; if let Err(err) = trace_fn() { error!(error = ?err, "Failed to parse raw JSON event received from Lambda. The handler will not be called. Log at TRACE level to see the payload."); - return RuntimeApiResponseFuture::Ready(Some(Err(err))); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(err)))); }; let request_id = req.context.request_id.clone(); + + // The invocation ID assigned by the Lambda runtime for cross-wiring protection. + // Echoed back on `/response` and `/error` to allow RAPID to reject stale responses + // from timed-out invocations. `None` when running against older RAPID versions + // that don't send this header. + let invocation_id = match req.parts.headers.get(LAMBDA_RUNTIME_INVOCATION_ID) { + Some(value) => match value.to_str() { + Ok(value) => Some(value.to_owned()), + Err(error) => { + rate_limited!(MALFORMED_INVOCATION_ID_LOG_INTERVAL, { + warn!( + error = ?error, + rate_limit_interval_ms = MALFORMED_INVOCATION_ID_LOG_INTERVAL.as_millis(), + "Ignoring malformed Lambda runtime invocation ID header" + ); + }); + None + } + }, + None => None, + }; + let lambda_event = match deserializer::deserialize::(&req.body, req.context) { Ok(lambda_event) => lambda_event, - Err(err) => match build_event_error_request(&request_id, err) { - Ok(request) => return RuntimeApiResponseFuture::Ready(Some(Ok(request))), + Err(err) => match build_event_error_request(request_id, invocation_id, err) { + Ok(request) => return RuntimeApiResponseFuture::Ready(Box::new(Some(Ok(request)))), Err(err) => { error!(error = ?err, "failed to build error response for Lambda Runtime API"); - return RuntimeApiResponseFuture::Ready(Some(Err(err))); + return RuntimeApiResponseFuture::Ready(Box::new(Some(Err(err)))); } }, }; - // Once the handler input has been generated successfully, the + // Once the handler input has been generated successfully, pass it through to inner services + // allowing processing both before reaching the handler function and after the handler completes. let fut = self.inner.call(lambda_event); - RuntimeApiResponseFuture::Future(fut, request_id, PhantomData) + RuntimeApiResponseFuture::Future(fut, request_id, invocation_id, PhantomData) } } -fn build_event_error_request(request_id: &str, err: T) -> Result, BoxError> +fn build_event_error_request( + request_id: String, + invocation_id: Option, + err: T, +) -> Result, BoxError> where T: Into + Debug, { error!(error = ?err, "Request payload deserialization into LambdaEvent failed. The handler will not be called. Log at TRACE level to see the payload."); - EventErrorRequest::new(request_id, err).into_req() + EventErrorRequest::new(&request_id, invocation_id.as_deref(), err).into_req() } #[pin_project(project = RuntimeApiResponseFutureProj)] @@ -132,6 +184,7 @@ pub enum RuntimeApiResponseFuture, PhantomData<( (), Response, @@ -141,7 +194,10 @@ pub enum RuntimeApiResponseFuture, ), - Ready(Option, BoxError>>), + /// This variant is used in case the invocation fails to be processed into an event. + /// We box it to avoid bloating the size of the more likely variant, which is + /// the future that drives event processing. + Ready(Box, BoxError>>>), } impl Future @@ -158,11 +214,76 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { task::Poll::Ready(match self.as_mut().project() { - RuntimeApiResponseFutureProj::Future(fut, request_id, _) => match ready!(fut.poll(cx)) { - Ok(ok) => EventCompletionRequest::new(request_id, ok).into_req(), - Err(err) => EventErrorRequest::new(request_id, err).into_req(), + RuntimeApiResponseFutureProj::Future(fut, request_id, invocation_id, _) => match ready!(fut.poll(cx)) { + Ok(ok) => EventCompletionRequest::new(request_id, invocation_id.as_deref(), ok).into_req(), + Err(err) => EventErrorRequest::new(request_id, invocation_id.as_deref(), err).into_req(), }, RuntimeApiResponseFutureProj::Ready(ready) => ready.take().expect("future polled after completion"), }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{constants::LAMBDA_RUNTIME_INVOCATION_ID, runtime::LambdaInvocation, Context}; + use http::{HeaderValue, Response}; + use serde_json::json; + use tower::{service_fn, Service}; + + #[tokio::test] + async fn forwards_invocation_id_from_next_response_headers() { + let mut response = Response::new(()); + response + .headers_mut() + .insert(LAMBDA_RUNTIME_INVOCATION_ID, HeaderValue::from_static("invocation-123")); + let (parts, _) = response.into_parts(); + + let mut service = RuntimeApiResponseService::new(service_fn(|_event: LambdaEvent| async { + Ok::<_, Diagnostic>(json!({"ok": true})) + })); + + let request = service + .call(LambdaInvocation { + parts, + body: bytes::Bytes::from_static(b"{}"), + context: Context::default(), + }) + .await + .expect("response request should be created"); + + assert_eq!( + request.headers().get(LAMBDA_RUNTIME_INVOCATION_ID), + Some(&HeaderValue::from_static("invocation-123")), + ); + } + + #[tokio::test] + async fn malformed_invocation_id_does_not_block_deserialization_error_response() { + let mut response = Response::new(()); + response.headers_mut().insert( + LAMBDA_RUNTIME_INVOCATION_ID, + HeaderValue::from_bytes(&[0xff]).expect("header value should accept opaque bytes"), + ); + let (parts, _) = response.into_parts(); + + let mut service = RuntimeApiResponseService::new(service_fn(|_event: LambdaEvent| async { + Ok::<_, Diagnostic>(json!({"ok": true})) + })); + + let request = service + .call(LambdaInvocation { + parts, + body: bytes::Bytes::from_static(b"{"), + context: Context { + request_id: "request-123".to_owned(), + ..Context::default() + }, + }) + .await + .expect("deserialization errors should produce an error request"); + + assert_eq!(request.uri().path(), "/2018-06-01/runtime/invocation/request-123/error",); + assert!(request.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } +} diff --git a/lambda-runtime/src/layers/mod.rs b/lambda-runtime/src/layers/mod.rs index 1f07f1995..37f52f472 100644 --- a/lambda-runtime/src/layers/mod.rs +++ b/lambda-runtime/src/layers/mod.rs @@ -4,7 +4,8 @@ mod api_response; mod panic; // Publicly available services. -mod trace; +/// Tracing utilities for Lambda runtime. +pub mod trace; pub(crate) use api_client::RuntimeApiClientService; pub(crate) use api_response::RuntimeApiResponseService; @@ -14,4 +15,5 @@ pub use trace::TracingLayer; #[cfg(feature = "opentelemetry")] mod otel; #[cfg(feature = "opentelemetry")] +#[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))] pub use otel::{OpenTelemetryFaasTrigger, OpenTelemetryLayer}; diff --git a/lambda-runtime/src/layers/otel.rs b/lambda-runtime/src/layers/otel.rs index f50f36f7b..0d874dcd6 100644 --- a/lambda-runtime/src/layers/otel.rs +++ b/lambda-runtime/src/layers/otel.rs @@ -1,7 +1,7 @@ use std::{fmt::Display, future::Future, pin::Pin, task}; use crate::LambdaInvocation; -use opentelemetry_semantic_conventions::trace as traceconv; +use opentelemetry_semantic_conventions::attribute; use pin_project::pin_project; use tower::{Layer, Service}; use tracing::{field, instrument::Instrumented, Instrument}; @@ -72,14 +72,28 @@ where } fn call(&mut self, req: LambdaInvocation) -> Self::Future { - let span = tracing::info_span!( - "Lambda function invocation", - "otel.name" = req.context.env_config.function_name, - "otel.kind" = field::Empty, - { traceconv::FAAS_TRIGGER } = &self.otel_attribute_trigger, - { traceconv::FAAS_INVOCATION_ID } = req.context.request_id, - { traceconv::FAAS_COLDSTART } = self.coldstart - ); + let span = if let Some(tenant_id) = &req.context.tenant_id { + tracing::info_span!( + "Lambda function invocation", + "otel.name" = req.context.env_config.function_name, + "otel.kind" = field::Empty, + { attribute::FAAS_NAME } = req.context.env_config.function_name, + { attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger, + { attribute::FAAS_INVOCATION_ID } = req.context.request_id, + { attribute::FAAS_COLDSTART } = self.coldstart, + "tenant_id" = tenant_id + ) + } else { + tracing::info_span!( + "Lambda function invocation", + "otel.name" = req.context.env_config.function_name, + "otel.kind" = field::Empty, + { attribute::FAAS_NAME } = req.context.env_config.function_name, + { attribute::FAAS_TRIGGER } = &self.otel_attribute_trigger, + { attribute::FAAS_INVOCATION_ID } = req.context.request_id, + { attribute::FAAS_COLDSTART } = self.coldstart + ) + }; // After the first execution, we can set 'coldstart' to false self.coldstart = false; @@ -131,7 +145,7 @@ where } /// Represent the possible values for the OpenTelemetry `faas.trigger` attribute. -/// See https://opentelemetry.io/docs/specs/semconv/attributes-registry/faas/ for more details. +/// See for more details. #[derive(Default, Clone, Copy)] #[non_exhaustive] pub enum OpenTelemetryFaasTrigger { @@ -159,3 +173,57 @@ impl Display for OpenTelemetryFaasTrigger { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Config, Context}; + use lambda_runtime_api_client::BoxError; + use std::sync::Arc; + use tower::service_fn; + use tracing_capture::{CaptureLayer, SharedStorage}; + use tracing_subscriber::layer::SubscriberExt; + + fn invocation_with_function_name(function_name: &str) -> LambdaInvocation { + let (parts, _) = http::Response::new(()).into_parts(); + LambdaInvocation { + parts, + body: bytes::Bytes::new(), + context: Context { + env_config: Arc::new(Config { + function_name: function_name.to_owned(), + ..Default::default() + }), + ..Default::default() + }, + } + } + + #[tokio::test] + async fn should_record_faas_name_from_the_function_configuration() { + // given + let storage = SharedStorage::default(); + let subscriber = tracing_subscriber::registry().with(CaptureLayer::new(&storage)); + let _guard = tracing::subscriber::set_default(subscriber); + let inner = service_fn(|_req: LambdaInvocation| async { Ok::<(), BoxError>(()) }); + let mut service = OpenTelemetryLayer::new(|| {}).layer(inner); + + // when + service + .call(invocation_with_function_name("my-function")) + .await + .expect("invocation should succeed"); + + // then + let storage = storage.lock(); + let span = storage + .all_spans() + .find(|span| span.metadata().name() == "Lambda function invocation") + .expect("otel span should be recorded"); + let faas_name = span.value("faas.name").expect("faas.name attribute should be recorded"); + assert_eq!( + *faas_name, "my-function", + "faas.name should match the function's configured name" + ); + } +} diff --git a/lambda-runtime/src/layers/panic.rs b/lambda-runtime/src/layers/panic.rs index 4b92e3c83..257a8f396 100644 --- a/lambda-runtime/src/layers/panic.rs +++ b/lambda-runtime/src/layers/panic.rs @@ -99,6 +99,8 @@ impl CatchPanicFuture<'_, F> { fn build_panic_diagnostic(err: &Box) -> Diagnostic { let error_message = if let Some(msg) = err.downcast_ref::<&str>() { format!("Lambda panicked: {msg}") + } else if let Some(msg) = err.downcast_ref::() { + format!("Lambda panicked: {msg}") } else { "Lambda panicked".to_string() }; diff --git a/lambda-runtime/src/layers/trace.rs b/lambda-runtime/src/layers/trace.rs index e93927b14..9980e8aa5 100644 --- a/lambda-runtime/src/layers/trace.rs +++ b/lambda-runtime/src/layers/trace.rs @@ -25,6 +25,7 @@ impl Layer for TracingLayer { } /// Tower service returned by [TracingLayer]. +#[derive(Clone)] pub struct TracingService { inner: S, } @@ -55,16 +56,35 @@ where /* ------------------------------------------- UTILS ------------------------------------------- */ -fn request_span(ctx: &Context) -> tracing::Span { - match &ctx.xray_trace_id { - Some(trace_id) => { +/// Creates a tracing span for a Lambda request with context information. +/// +/// This function creates a span that includes the request ID and optionally +/// the X-Ray trace ID and tenant ID if they are available in the context. +pub fn request_span(ctx: &Context) -> tracing::Span { + match (&ctx.xray_trace_id, &ctx.tenant_id) { + (Some(trace_id), Some(tenant_id)) => { + tracing::info_span!( + "Lambda runtime invoke", + requestId = &ctx.request_id, + xrayTraceId = trace_id, + tenantId = tenant_id + ) + } + (Some(trace_id), None) => { tracing::info_span!( "Lambda runtime invoke", requestId = &ctx.request_id, xrayTraceId = trace_id ) } - None => { + (None, Some(tenant_id)) => { + tracing::info_span!( + "Lambda runtime invoke", + requestId = &ctx.request_id, + tenantId = tenant_id + ) + } + (None, None) => { tracing::info_span!("Lambda runtime invoke", requestId = &ctx.request_id) } } diff --git a/lambda-runtime/src/lib.rs b/lambda-runtime/src/lib.rs index 76d0562ae..ce6688baa 100644 --- a/lambda-runtime/src/lib.rs +++ b/lambda-runtime/src/lib.rs @@ -1,6 +1,7 @@ #![deny(clippy::all, clippy::cargo)] #![allow(clippy::multiple_crate_versions)] #![warn(missing_docs, nonstandard_style, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg))] //! The mechanism available for defining a Lambda function is as follows: //! @@ -12,26 +13,37 @@ use std::{ env, fmt::{self, Debug}, future::Future, + pin::Pin, sync::Arc, }; use tokio_stream::Stream; use tower::util::ServiceFn; pub use tower::{self, service_fn, Service}; +#[macro_use] +mod macros; + +mod constants; + /// Diagnostic utilities to convert Rust types into Lambda Error types. pub mod diagnostic; pub use diagnostic::Diagnostic; +mod rate_limiter; + mod deserializer; /// Tower middleware to be applied to runtime invocations. pub mod layers; mod requests; mod runtime; +/// SnapStart snapshot/restore lifecycle support. +pub mod snapstart; /// Utilities for Lambda Streaming functions. pub mod streaming; /// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions. #[cfg(feature = "tracing")] +#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))] pub use lambda_runtime_api_client::tracing; /// Types available to a Lambda function. @@ -39,8 +51,14 @@ mod types; use requests::EventErrorRequest; pub use runtime::{LambdaInvocation, Runtime}; +pub use snapstart::SnapStartResource; pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse}; +/// A boxed, `Send` future with a bound lifetime — the return type of +/// [`SnapStartResource`] hooks. Inlined (rather than re-exported from `futures`) +/// to avoid leaking the `futures` crate into this crate's public type surface. +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; + /// Error type that lambdas may result in pub type Error = lambda_runtime_api_client::BoxError; @@ -92,6 +110,13 @@ where /// If you need more control over the runtime and add custom middleware, use the /// [Runtime] type directly. /// +/// # Managed concurrency +/// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. +/// If your handler can satisfy `Clone + Send + 'static`, +/// prefer [`run_concurrent`] (requires the `concurrency-tokio` feature), +/// which honors managed concurrency and falls back to sequential behavior when +/// unset. +/// /// # Example /// ```no_run /// use lambda_runtime::{Error, service_fn, LambdaEvent}; @@ -108,6 +133,12 @@ where /// Ok(event.payload) /// } /// ``` +/// +/// # Panics +/// +/// This function panics if required Lambda environment variables are missing +/// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, +/// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). pub async fn run(handler: F) -> Result<(), Error> where F: Service, Response = R>, @@ -123,3 +154,168 @@ where let runtime = Runtime::new(handler).layer(layers::TracingLayer::new()); runtime.run().await } + +/// Starts the Lambda Rust runtime in a mode that is compatible with +/// Lambda Managed Instances (concurrent invocations). +/// +/// Requires the `concurrency-tokio` feature. +/// +/// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this +/// will spawn `AWS_LAMBDA_MAX_CONCURRENCY` tokio worker tasks, each running its own +/// `/next` polling loop. When the environment variable is unset or `<= 1`, it +/// falls back to the same sequential behavior as [`run`], so the same handler +/// can run on both classic Lambda and Lambda Managed Instances. +/// +/// If you need more control over the runtime and add custom middleware, use the +/// [Runtime] type directly. +/// +/// # Example +/// ```no_run +/// use lambda_runtime::{Error, service_fn, LambdaEvent}; +/// use serde_json::Value; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let func = service_fn(func); +/// lambda_runtime::run_concurrent(func).await?; +/// Ok(()) +/// } +/// +/// async fn func(event: LambdaEvent) -> Result { +/// Ok(event.payload) +/// } +/// ``` +/// +/// # Panics +/// +/// This function panics if: +/// - Called outside of a Tokio runtime +/// - Required Lambda environment variables are missing (`AWS_LAMBDA_FUNCTION_NAME`, +/// `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, `AWS_LAMBDA_FUNCTION_VERSION`, +/// `AWS_LAMBDA_RUNTIME_API`) +#[cfg(feature = "concurrency-tokio")] +#[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] +pub async fn run_concurrent(handler: F) -> Result<(), Error> +where + F: Service, Response = R> + Clone + Send + 'static, + F::Future: Future> + Send + 'static, + F::Error: Into + fmt::Debug, + A: for<'de> Deserialize<'de> + Send + 'static, + R: IntoFunctionResponse + Send + 'static, + B: Serialize + Send + 'static, + S: Stream> + Unpin + Send + 'static, + D: Into + Send + 'static, + E: Into + Send + Debug + 'static, +{ + let runtime = Runtime::new(handler).layer(layers::TracingLayer::new()); + runtime.run_concurrent().await +} + +/// Spawns a task that will be execute a provided async closure when the process +/// receives unix graceful shutdown signals. If the closure takes longer than 500ms +/// to execute, an unhandled `SIGKILL` signal might be received. +/// +/// You can use this future to execute cleanup or flush related logic prior to runtime shutdown. +/// +/// This function's returned future must be resolved prior to `lambda_runtime::run()`. +/// +/// Note that this implicitly also registers and drives a no-op internal extension that subscribes to no events. +/// This extension will be named `_lambda-rust-runtime-no-op-graceful-shutdown-helper`. This extension name +/// can not be reused by other registered extensions. This is necessary in order to receive graceful shutdown signals. +/// +/// This extension is cheap to run because it receives no events, but is not zero cost. If you have another extension +/// registered already, you might prefer to manually construct your own graceful shutdown handling without the dummy extension. +/// +/// For more information on general AWS Lambda graceful shutdown handling, see: +/// +/// +/// # Panics +/// +/// This function panics if: +/// - this function is called after `lambda_runtime::run()` +/// - this function is called outside of a context that has access to the tokio i/o +/// - the no-op extension cannot be registered +/// - either signal listener panics [tokio::signal::unix](https://docs.rs/tokio/latest/tokio/signal/unix/fn.signal.html#errors) +/// +/// # Example +/// ```no_run +/// use lambda_runtime::{Error, service_fn, LambdaEvent}; +/// use serde_json::Value; +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Error> { +/// let func = service_fn(func); +/// +/// let (writer, log_guard) = tracing_appender::non_blocking(std::io::stdout()); +/// lambda_runtime::tracing::init_default_subscriber_with_writer(writer); +/// +/// let shutdown_hook = || async move { +/// std::mem::drop(log_guard); +/// }; +/// lambda_runtime::spawn_graceful_shutdown_handler(shutdown_hook).await; +/// +/// lambda_runtime::run(func).await?; +/// Ok(()) +/// } +/// +/// async fn func(event: LambdaEvent) -> Result { +/// Ok(event.payload) +/// } +/// ``` +#[cfg(all(unix, feature = "graceful-shutdown"))] +#[cfg_attr(docsrs, doc(cfg(all(unix, feature = "graceful-shutdown"))))] +pub async fn spawn_graceful_shutdown_handler(shutdown_hook: impl FnOnce() -> Fut + Send + 'static) +where + Fut: Future + Send + 'static, +{ + // You need an extension registered with the Lambda orchestrator in order for your process + // to receive a SIGTERM for graceful shutdown. + // + // We accomplish this here by registering a no-op internal extension, which does not subscribe to any events. + // + // This extension is cheap to run since after it connects to the lambda orchestration, the connection + // will just wait forever for data to come, which never comes, so it won't cause wakes. + let extension = lambda_extension::Extension::new() + // Don't subscribe to any event types + .with_events(&[]) + // Internal extension names MUST be unique within a given Lambda function. + .with_extension_name("_lambda-rust-runtime-no-op-graceful-shutdown-helper") + // Extensions MUST be registered before calling lambda_runtime::run(), which ends the Init + // phase and begins the Invoke phase. + .register() + .await + .expect("could not register no-op extension for graceful shutdown"); + + tokio::task::spawn(async move { + let graceful_shutdown_future = async move { + let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt()).unwrap(); + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).unwrap(); + tokio::select! { + _sigint = sigint.recv() => { + eprintln!("[runtime] SIGINT received"); + eprintln!("[runtime] Graceful shutdown in progress ..."); + shutdown_hook().await; + eprintln!("[runtime] Graceful shutdown completed"); + std::process::exit(0); + }, + _sigterm = sigterm.recv()=> { + eprintln!("[runtime] SIGTERM received"); + eprintln!("[runtime] Graceful shutdown in progress ..."); + shutdown_hook().await; + eprintln!("[runtime] Graceful shutdown completed"); + std::process::exit(0); + }, + } + }; + + let _: (_, ()) = tokio::join!( + // we always poll the graceful shutdown future first, + // which results in a smaller future due to lack of bookkeeping of which was last polled + biased; + graceful_shutdown_future, async { + // we suppress extension errors because we don't actually mind if it crashes, + // all we need to do is kick off the run so that lambda exits the init phase + let _ = extension.run().await; + }); + }); +} diff --git a/lambda-runtime/src/macros.rs b/lambda-runtime/src/macros.rs new file mode 100644 index 000000000..8c531470d --- /dev/null +++ b/lambda-runtime/src/macros.rs @@ -0,0 +1,10 @@ +// Logs using tracing `error!` if a dispatcher is set, otherwise falls back to `eprintln!`. +macro_rules! log_or_print { + (tracing: $tracing_expr:expr, fallback: $fallback_expr:expr) => { + if tracing::dispatcher::has_been_set() { + $tracing_expr; + } else { + $fallback_expr; + } + }; +} diff --git a/lambda-runtime/src/rate_limiter.rs b/lambda-runtime/src/rate_limiter.rs new file mode 100644 index 000000000..0c4bd6765 --- /dev/null +++ b/lambda-runtime/src/rate_limiter.rs @@ -0,0 +1,88 @@ +//! Internal call-site rate limiting for infrequent runtime events. + +use std::{ + sync::OnceLock, + time::{Duration, Instant}, +}; + +/// Returns monotonic process-relative time for the rate limiter. +pub(crate) fn time_since_epoch() -> Duration { + static EPOCH: OnceLock = OnceLock::new(); + + Instant::now().duration_since(*EPOCH.get_or_init(Instant::now)) +} + +/// Evaluates a call at most once per interval at each macro call site. +/// +/// The limiter state is local to the call site and persists for the lifetime of +/// the process. It is therefore shared across warm invocations in one Lambda +/// execution environment and reset by a cold start. +macro_rules! rate_limited { + ($interval:expr, $call:expr) => {{ + use std::sync::atomic::{AtomicU64, Ordering}; + + static NEXT_CALL: AtomicU64 = AtomicU64::new(u64::MIN); + let interval: std::time::Duration = $interval; + let time = $crate::rate_limiter::time_since_epoch(); + let next = NEXT_CALL.load(Ordering::Relaxed); + + if next <= time.as_secs() { + let new_next = time.checked_add(interval).unwrap_or(std::time::Duration::MAX).as_secs(); + + if NEXT_CALL + .compare_exchange(next, new_next, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + $call; + } + } + }}; +} + +pub(crate) use rate_limited; + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + thread, + }; + + #[test] + fn allows_first_call_and_rejects_calls_inside_interval() { + let mut calls = 0; + + for _ in 0..2 { + rate_limited!(Duration::from_secs(60), { + calls += 1; + }); + } + + assert_eq!(calls, 1); + } + + #[test] + fn allows_only_one_concurrent_call() { + let calls = Arc::new(AtomicUsize::new(0)); + let handles = (0..8) + .map(|_| { + let calls = Arc::clone(&calls); + thread::spawn(move || { + rate_limited!(Duration::from_secs(60), { + calls.fetch_add(1, Ordering::Relaxed); + }); + }) + }) + .collect::>(); + + for handle in handles { + handle.join().unwrap(); + } + + assert_eq!(calls.load(Ordering::Relaxed), 1); + } +} diff --git a/lambda-runtime/src/requests.rs b/lambda-runtime/src/requests.rs index e8b0183cf..933e3100a 100644 --- a/lambda-runtime/src/requests.rs +++ b/lambda-runtime/src/requests.rs @@ -1,4 +1,7 @@ -use crate::{types::ToStreamErrorTrailer, Diagnostic, Error, FunctionResponse, IntoFunctionResponse}; +use crate::{ + constants::LAMBDA_RUNTIME_INVOCATION_ID, types::ToStreamErrorTrailer, Diagnostic, Error, FunctionResponse, + IntoFunctionResponse, +}; use bytes::Bytes; use http::{header::CONTENT_TYPE, Method, Request, Uri}; use lambda_runtime_api_client::{body::Body, build_request}; @@ -24,18 +27,58 @@ impl IntoRequest for NextEventRequest { } } +// /runtime/restore/next #[derive(Debug, Eq, PartialEq)] -pub struct NextEventResponse<'a> { - // lambda-runtime-aws-request-id - pub request_id: &'a str, - // lambda-runtime-deadline-ms - pub deadline: u64, - // lambda-runtime-invoked-function-arn - pub arn: &'a str, - // lambda-runtime-trace-id - pub trace_id: &'a str, - // the actual body, - pub body: Vec, +pub(crate) struct RestoreNextRequest; + +impl IntoRequest for RestoreNextRequest { + fn into_req(self) -> Result, Error> { + let req = build_request() + .method(Method::GET) + .uri(Uri::from_static("/2018-06-01/runtime/restore/next")) + .body(Default::default())?; + Ok(req) + } +} + +// /runtime/init/error +pub(crate) struct InitErrorRequest { + pub(crate) diagnostic: Diagnostic, +} + +impl IntoRequest for InitErrorRequest { + fn into_req(self) -> Result, Error> { + let uri = Uri::from_static("/2018-06-01/runtime/init/error"); + let body = serde_json::to_vec(&self.diagnostic)?; + let body = Body::from(body); + + let req = build_request() + .method(Method::POST) + .uri(uri) + .header("lambda-runtime-function-error-type", "Runtime.InitError") + .body(body)?; + Ok(req) + } +} + +// /runtime/restore/error +pub(crate) struct RestoreErrorRequest { + pub(crate) diagnostic: Diagnostic, +} + +impl IntoRequest for RestoreErrorRequest { + fn into_req(self) -> Result, Error> { + let uri = Uri::from_static("/2018-06-01/runtime/restore/error"); + let body = serde_json::to_vec(&self.diagnostic)?; + let body = Body::from(body); + + let req = build_request() + .method(Method::POST) + .uri(uri) + .header("lambda-runtime-function-error-type", "Runtime.AfterRestoreError") + .body(body)?; + Ok(req) + } } // /runtime/invocation/{AwsRequestId}/response @@ -48,6 +91,7 @@ where E: Into + Send + Debug, { pub(crate) request_id: &'a str, + pub(crate) invocation_id: Option<&'a str>, pub(crate) body: R, pub(crate) _unused_b: PhantomData, pub(crate) _unused_s: PhantomData, @@ -62,9 +106,14 @@ where E: Into + Send + Debug, { /// Initialize a new EventCompletionRequest - pub(crate) fn new(request_id: &'a str, body: R) -> EventCompletionRequest<'a, R, B, S, D, E> { + pub(crate) fn new( + request_id: &'a str, + invocation_id: Option<&'a str>, + body: R, + ) -> EventCompletionRequest<'a, R, B, S, D, E> { EventCompletionRequest { request_id, + invocation_id, body, _unused_b: PhantomData::, _unused_s: PhantomData::, @@ -89,7 +138,12 @@ where let body = serde_json::to_vec(&body)?; let body = Body::from(body); - let req = build_request().method(Method::POST).uri(uri).body(body)?; + let mut req = build_request().method(Method::POST).uri(uri).body(body)?; + + if let Some(id) = self.invocation_id { + req.headers_mut().insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + Ok(req) } FunctionResponse::StreamingResponse(mut response) => { @@ -105,6 +159,11 @@ where // See the details in Lambda Developer Doc: https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-response-streaming req_headers.append("Trailer", "Lambda-Runtime-Function-Error-Type".parse()?); req_headers.append("Trailer", "Lambda-Runtime-Function-Error-Body".parse()?); + + if let Some(id) = self.invocation_id { + req_headers.insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + req_headers.insert( "Content-Type", "application/vnd.awslambda.http-integration-response".parse()?, @@ -123,15 +182,26 @@ where let (mut tx, rx) = Body::channel(); tokio::spawn(async move { - tx.send_data(metadata_prelude.into()).await.unwrap(); - tx.send_data("\u{0}".repeat(8).into()).await.unwrap(); + if tx.send_data(metadata_prelude.into()).await.is_err() { + tracing::error!("Error sending metadata prelude, response channel closed"); + return; + } + + if tx.send_data("\u{0}".repeat(8).into()).await.is_err() { + tracing::error!("Error sending metadata prelude delimiter, response channel closed"); + return; + } while let Some(chunk) = response.stream.next().await { let chunk = match chunk { Ok(chunk) => chunk.into(), Err(err) => err.into().to_tailer().into(), }; - tx.send_data(chunk).await.unwrap(); + + if tx.send_data(chunk).await.is_err() { + tracing::error!("Error sending response body chunk, response channel closed"); + return; + } } }); @@ -142,29 +212,22 @@ where } } -#[test] -fn test_event_completion_request() { - let req = EventCompletionRequest::new("id", "hello, world!"); - let req = req.into_req().unwrap(); - let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); - assert_eq!(req.method(), Method::POST); - assert_eq!(req.uri(), &expected); - assert!(match req.headers().get("User-Agent") { - Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), - None => false, - }); -} - // /runtime/invocation/{AwsRequestId}/error pub(crate) struct EventErrorRequest<'a> { pub(crate) request_id: &'a str, + pub(crate) invocation_id: Option<&'a str>, pub(crate) diagnostic: Diagnostic, } impl<'a> EventErrorRequest<'a> { - pub(crate) fn new(request_id: &'a str, diagnostic: impl Into) -> EventErrorRequest<'a> { + pub(crate) fn new( + request_id: &'a str, + invocation_id: Option<&'a str>, + diagnostic: impl Into, + ) -> EventErrorRequest<'a> { EventErrorRequest { request_id, + invocation_id, diagnostic: diagnostic.into(), } } @@ -177,11 +240,16 @@ impl IntoRequest for EventErrorRequest<'_> { let body = serde_json::to_vec(&self.diagnostic)?; let body = Body::from(body); - let req = build_request() + let mut req = build_request() .method(Method::POST) .uri(uri) .header("lambda-runtime-function-error-type", "unhandled") .body(body)?; + + if let Some(id) = self.invocation_id { + req.headers_mut().insert(LAMBDA_RUNTIME_INVOCATION_ID, id.parse()?); + } + Ok(req) } } @@ -202,10 +270,92 @@ mod tests { }); } + #[test] + fn test_event_completion_request() { + let req = EventCompletionRequest::new("id", Option::Some("invocation_id"), "hello, world!"); + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + + assert!(req + .headers() + .get("User-Agent") + .unwrap() + .to_str() + .unwrap() + .starts_with("aws-lambda-rust/")); + + assert_eq!( + req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).unwrap(), + "invocation_id" + ); + } + + #[test] + fn test_event_completion_request_invocation_id_not_added_when_none() { + let req = EventCompletionRequest::new("id", Option::None, "hello, world!"); + let req = req.into_req().unwrap(); + + assert!(req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } + + #[test] + fn test_streaming_event_completion_request_with_invocation_id() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = + EventCompletionRequest::new("id", Some("invocation_id"), response); + + let http_req = req.into_req().expect("into_req should succeed"); + let expected = Uri::from_static("/2018-06-01/runtime/invocation/id/response"); + assert_eq!(http_req.method(), Method::POST); + assert_eq!(http_req.uri(), &expected); + + assert_eq!( + http_req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).unwrap(), + "invocation_id" + ); + }); + } + + #[test] + fn test_streaming_event_completion_request_invocation_id_not_added_when_none() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = EventCompletionRequest::new("id", None, response); + + let http_req = req.into_req().expect("into_req should succeed"); + + assert!(http_req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + }); + } + #[test] fn test_event_error_request() { let req = EventErrorRequest { request_id: "id", + invocation_id: Option::Some("invocation_id"), diagnostic: Diagnostic { error_type: "InvalidEventDataError".into(), error_message: "Error parsing event data".into(), @@ -219,5 +369,114 @@ mod tests { Some(header) => header.to_str().unwrap().starts_with("aws-lambda-rust/"), None => false, }); + + assert!(match req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID) { + Some(header) => header.to_str().unwrap() == "invocation_id", + None => false, + }); + } + + #[test] + fn test_event_error_request_invocation_id_not_added_when_none() { + let req = EventErrorRequest { + request_id: "id", + invocation_id: None, + diagnostic: Diagnostic { + error_type: "InvalidEventDataError".into(), + error_message: "Error parsing event data".into(), + }, + }; + let req = req.into_req().unwrap(); + + assert!(req.headers().get(LAMBDA_RUNTIME_INVOCATION_ID).is_none()); + } + + #[test] + fn test_restore_next_request() { + let req = RestoreNextRequest; + let req = req.into_req().unwrap(); + assert_eq!(req.method(), Method::GET); + assert_eq!(req.uri(), &Uri::from_static("/2018-06-01/runtime/restore/next")); + assert!(req + .headers() + .get("User-Agent") + .unwrap() + .to_str() + .unwrap() + .starts_with("aws-lambda-rust/")); + } + + #[test] + fn test_restore_error_request() { + let req = RestoreErrorRequest { + diagnostic: Diagnostic { + error_type: "Runtime.AfterRestoreError".into(), + error_message: "Failed to reconnect to database".into(), + }, + }; + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/restore/error"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + assert_eq!( + req.headers().get("lambda-runtime-function-error-type").unwrap(), + "Runtime.AfterRestoreError" + ); + } + + #[test] + fn test_init_error_request() { + let req = InitErrorRequest { + diagnostic: Diagnostic { + error_type: "Runtime.InitError".into(), + error_message: "Failed to release resources before snapshot".into(), + }, + }; + let req = req.into_req().unwrap(); + let expected = Uri::from_static("/2018-06-01/runtime/init/error"); + assert_eq!(req.method(), Method::POST); + assert_eq!(req.uri(), &expected); + assert_eq!( + req.headers().get("lambda-runtime-function-error-type").unwrap(), + "Runtime.InitError" + ); + } + + #[test] + #[cfg(tokio_unstable)] + fn streaming_send_data_error_is_ignored() { + use crate::StreamResponse; + + let runtime = tokio::runtime::Builder::new_current_thread() + .unhandled_panic(tokio::runtime::UnhandledPanic::ShutdownRuntime) + .enable_all() + .build() + .unwrap(); + + // We don't want to use a global panic hook to avoid shared state across tests, but we need to know + // if a background task panics. We accomplish this by using UnhandledPanic::ShutdownRuntime + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + runtime.block_on(async { + let stream = tokio_stream::iter(vec![Ok::(Bytes::from_static(b"chunk"))]); + + let stream_response: StreamResponse<_> = stream.into(); + let response = FunctionResponse::StreamingResponse(stream_response); + + let req: EventCompletionRequest<'_, _, (), _, _, _> = EventCompletionRequest::new("id", None, response); + + let http_req = req.into_req().expect("into_req should succeed"); + + // immediate drop simulates client disconnection + drop(http_req); + + // give the spawned task time to complete + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + }) + })); + + assert!( + result.is_ok(), + "spawned task panicked - send_data errors should be ignored" + ); } } diff --git a/lambda-runtime/src/runtime.rs b/lambda-runtime/src/runtime.rs index 5749fbb79..f31bc4f24 100644 --- a/lambda-runtime/src/runtime.rs +++ b/lambda-runtime/src/runtime.rs @@ -1,16 +1,23 @@ use crate::{ layers::{CatchPanicService, RuntimeApiClientService, RuntimeApiResponseService}, - requests::{IntoRequest, NextEventRequest}, + requests::{InitErrorRequest, IntoRequest, NextEventRequest, RestoreErrorRequest, RestoreNextRequest}, + snapstart::SnapStartResource, types::{invoke_request_id, IntoFunctionResponse, LambdaEvent}, Config, Context, Diagnostic, }; +#[cfg(feature = "concurrency-tokio")] +use futures::stream::FuturesUnordered; use http_body_util::BodyExt; -use lambda_runtime_api_client::{BoxError, Client as ApiClient}; +use lambda_runtime_api_client::{BoxError, PooledClient as ApiClient}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "concurrency-tokio")] +use std::fmt; use std::{env, fmt::Debug, future::Future, sync::Arc}; use tokio_stream::{Stream, StreamExt}; use tower::{Layer, Service, ServiceExt}; use tracing::trace; +#[cfg(feature = "concurrency-tokio")] +use tracing::{debug, error, info_span, warn, Instrument}; /* ----------------------------------------- INVOCATION ---------------------------------------- */ @@ -53,6 +60,8 @@ pub struct Runtime { service: S, config: Arc, client: Arc, + concurrency_limit: u32, + snapstart_resources: Vec>, } impl @@ -67,6 +76,7 @@ impl, + ApiClient, >, > where @@ -89,14 +99,26 @@ where /// Note that manually creating a [Runtime] does not add tracing to the executed handler /// as is done by [super::run]. If you want to add the default tracing functionality, call /// [Runtime::layer] with a [super::layers::TracingLayer]. + /// + /// + /// # Panics + /// + /// This function panics if required Lambda environment variables are missing + /// (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, + /// `AWS_LAMBDA_FUNCTION_VERSION`, `AWS_LAMBDA_RUNTIME_API`). pub fn new(handler: F) -> Self { trace!("Loading config from env"); let config = Arc::new(Config::from_env()); - let client = Arc::new(ApiClient::builder().build().expect("Unable to create a runtime client")); + let concurrency_limit = max_concurrency_from_env().unwrap_or(1).max(1); + // Strategy: allocate all worker tasks up-front, so size the client pool to match. + let pool_size = concurrency_limit as usize; + let client = Arc::new(ApiClient::builder().with_pool_size(pool_size).build()); Self { service: wrap_handler(handler, client.clone()), config, client, + concurrency_limit, + snapstart_resources: Vec::new(), } } } @@ -133,16 +155,278 @@ impl Runtime { client: self.client, config: self.config, service: layer.layer(self.service), + concurrency_limit: self.concurrency_limit, + snapstart_resources: self.snapstart_resources, } } } +impl Runtime { + /// Returns `true` if the current execution environment has SnapStart enabled. + pub fn is_snapstart(&self) -> bool { + is_snapstart_env() + } + + /// Register a [`SnapStartResource`] whose `before_snapshot`/`after_restore` + /// hooks should run around the SnapStart snapshot/restore boundary. + /// + /// Register resources in dependency order — **foundations first** (e.g. + /// credentials before the pool that depends on them). The runtime runs + /// `before_snapshot` in reverse registration order (LIFO) and `after_restore` + /// in registration order (FIFO), so teardown and rebuild both happen in the + /// correct relative order. See the [`snapstart`](crate::snapstart) module + /// docs for details. + /// + /// When SnapStart is not enabled, registered resources are never invoked and + /// add no runtime overhead beyond the cost of holding the `Arc`. + /// + /// # Example + /// ```no_run + /// use lambda_runtime::{Error, LambdaEvent, Runtime, SnapStartResource}; + /// use std::sync::Arc; + /// use serde_json::Value; + /// use tower::service_fn; + /// + /// // Uses the default no-op hooks; override before_snapshot/after_restore as needed. + /// struct Pool; + /// impl SnapStartResource for Pool {} + /// + /// #[tokio::main] + /// async fn main() -> Result<(), Error> { + /// let pool = Arc::new(Pool); + /// let runtime = Runtime::new(service_fn(handler)) + /// .register_snapstart_resource(pool.clone()); + /// runtime.run().await + /// } + /// + /// async fn handler(event: LambdaEvent) -> Result { + /// Ok(event.payload) + /// } + /// ``` + pub fn register_snapstart_resource(mut self, resource: Arc) -> Self { + self.snapstart_resources.push(resource); + self + } +} + +#[cfg(feature = "concurrency-tokio")] +impl Runtime +where + S: Service + Clone + Send + 'static, + S::Future: Send, +{ + /// Start the runtime and begin polling for events on the Lambda Runtime API, + /// in a mode that is compatible with Lambda Managed Instances. + /// + /// When `AWS_LAMBDA_MAX_CONCURRENCY` is set to a value greater than 1, this + /// spawns multiple tokio worker tasks to handle concurrent invocations. When the + /// environment variable is unset or `<= 1`, it falls back to sequential + /// behavior, so the same handler can run on both classic Lambda and Lambda + /// Managed Instances. + /// + /// # Panics + /// + /// This function panics if called outside of a Tokio runtime. + #[cfg_attr(docsrs, doc(cfg(feature = "concurrency-tokio")))] + pub async fn run_concurrent(self) -> Result<(), BoxError> { + if tokio::runtime::Handle::try_current().is_err() { + panic!("`run_concurrent` must be called from within a Tokio runtime"); + } + + snapstart_lifecycle(&self.client, &self.snapstart_resources).await?; + + if self.concurrency_limit > 1 { + trace!("Concurrent mode: _X_AMZN_TRACE_ID is not set; use context.xray_trace_id"); + Self::run_concurrent_inner(self.service, self.config, self.client, self.concurrency_limit).await + } else { + debug!( + "Concurrent polling disabled (AWS_LAMBDA_MAX_CONCURRENCY unset or <= 1); falling back to sequential polling" + ); + let incoming = incoming(&self.client); + Self::run_with_incoming(self.service, self.config, incoming).await + } + } + + /// Concurrent processing using N independent long-poll loops (for Lambda managed-concurrency). + async fn run_concurrent_inner( + service: S, + config: Arc, + client: Arc, + concurrency_limit: u32, + ) -> Result<(), BoxError> { + let limit = concurrency_limit as usize; + + debug!( + event = "runtime_worker_pool_initializing", + worker_count = limit, + execution_environment_max_concurrency = limit, + ); + + // Use FuturesUnordered so we can observe worker exits as they happen, + // rather than waiting for all workers to finish (join_all). + let mut workers: FuturesUnordered)>> = + FuturesUnordered::new(); + let spawn_worker = |service: S, config: Arc, client: Arc| { + tokio::spawn(async move { + let task_id = tokio::task::id(); + let result = concurrent_worker_loop(service, config, client).await; + (task_id, result) + }) + }; + // Spawn one worker per concurrency slot; the last uses the owned service to avoid an extra clone. + for _ in 1..limit { + workers.push(spawn_worker(service.clone(), config.clone(), client.clone())); + } + workers.push(spawn_worker(service, config, client)); + + // Track worker exits across tasks. A single worker failing should not + // terminate the whole runtime (LMI keeps running with the remaining + // healthy workers). We only return an error once there are no workers + // left (i.e., we cannot keep at least 1 worker alive). + // + // Note: Handler errors (Err returned from user code) do NOT trigger this. + // They are reported to Lambda via /invocation/{id}/error and the worker + // continues. This only captures unrecoverable runtime failures like + // API client failures, runtime panics, etc. + let mut errors: Vec = Vec::new(); + let mut remaining_workers = limit; + while let Some(result) = futures::StreamExt::next(&mut workers).await { + remaining_workers = remaining_workers.saturating_sub(1); + match result { + Ok((task_id, Ok(()))) => { + // `concurrent_worker_loop` runs indefinitely, so an Ok return indicates + // an unexpected worker exit; we still decrement because the task is gone. + error!( + task_id = %task_id, + remaining_workers, + "Concurrent worker exited cleanly (unexpected - loop should run forever)" + ); + errors.push(WorkerError::CleanExit(task_id)); + } + Ok((task_id, Err(err))) => { + error!( + task_id = %task_id, + error = %err, + remaining_workers, + "Concurrent worker exited with error" + ); + errors.push(WorkerError::Failure(task_id, err)); + } + Err(join_err) => { + let task_id = join_err.id(); + let err: BoxError = Box::new(join_err); + error!( + task_id = %task_id, + error = %err, + remaining_workers, + "Concurrent worker panicked" + ); + errors.push(WorkerError::Failure(task_id, err)); + } + } + } + + match errors.len() { + 0 => Ok(()), + _ => Err(Box::new(ConcurrentWorkerErrors { errors })), + } + } +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Debug)] +enum WorkerError { + CleanExit(tokio::task::Id), + Failure(tokio::task::Id, BoxError), +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Debug)] +struct ConcurrentWorkerErrors { + errors: Vec, +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Serialize)] +struct ConcurrentWorkerErrorsPayload<'a> { + message: &'a str, + #[serde(skip_serializing_if = "Vec::is_empty")] + clean: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + failures: Vec, +} + +#[cfg(feature = "concurrency-tokio")] +#[derive(Serialize)] +struct WorkerFailurePayload { + id: String, + err: String, +} + +#[cfg(feature = "concurrency-tokio")] +impl fmt::Display for ConcurrentWorkerErrors { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut clean = Vec::new(); + let mut failures = Vec::new(); + for error in &self.errors { + match error { + WorkerError::CleanExit(task_id) => clean.push(task_id), + WorkerError::Failure(task_id, err) => failures.push((task_id, err)), + } + } + + let clean_ids: Vec = clean.iter().map(|task_id| task_id.to_string()).collect(); + let failure_entries: Vec = failures + .iter() + .map(|(task_id, err)| WorkerFailurePayload { + id: task_id.to_string(), + err: err.to_string(), + }) + .collect(); + + let message = if failures.is_empty() && !clean.is_empty() { + "all concurrent workers exited cleanly (unexpected - loop should run forever)" + } else { + "concurrent workers exited unexpectedly" + }; + + let payload = ConcurrentWorkerErrorsPayload { + message, + clean: clean_ids, + failures: failure_entries, + }; + let json = serde_json::to_string(&payload).map_err(|_| fmt::Error)?; + write!(f, "{json}") + } +} + +#[cfg(feature = "concurrency-tokio")] +impl std::error::Error for ConcurrentWorkerErrors {} + impl Runtime where S: Service, { /// Start the runtime and begin polling for events on the Lambda Runtime API. + /// + /// The runtime will process requests sequentially. + /// + /// # Managed concurrency + /// If `AWS_LAMBDA_MAX_CONCURRENCY` is set, a warning is logged. + /// If your handler can satisfy `Clone + Send + 'static`, + /// prefer [`Runtime::run_concurrent`] (requires the `concurrency-tokio` feature), + /// which honors managed concurrency and falls back to sequential behavior when + /// unset. pub async fn run(self) -> Result<(), BoxError> { + if let Some(raw) = concurrency_env_value() { + log_or_print!( + tracing: tracing::warn!( + "AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially", + ), + fallback: eprintln!("AWS_LAMBDA_MAX_CONCURRENCY is set to '{raw}', but the concurrency-tokio feature is not enabled; running sequentially") + ); + } + snapstart_lifecycle(&self.client, &self.snapstart_resources).await?; let incoming = incoming(&self.client); Self::run_with_incoming(self.service, self.config, incoming).await } @@ -158,36 +442,143 @@ where while let Some(next_event_response) = incoming.next().await { trace!("New event arrived (run loop)"); let event = next_event_response?; - let (parts, incoming) = event.into_parts(); + process_invocation(&mut service, &config, event, true).await?; + } + Ok(()) + } +} - #[cfg(debug_assertions)] - if parts.status == http::StatusCode::NO_CONTENT { - // Ignore the event if the status code is 204. - // This is a way to keep the runtime alive when - // there are no events pending to be processed. - continue; - } +/* ------------------------------------------- UTILS ------------------------------------------- */ - // Build the invocation such that it can be sent to the service right away - // when it is ready - let body = incoming.collect().await?.to_bytes(); - let context = Context::new(invoke_request_id(&parts.headers)?, config.clone(), &parts.headers)?; - let invocation = LambdaInvocation { parts, body, context }; +/// Returns `true` if the current execution environment has SnapStart enabled. +fn is_snapstart_env() -> bool { + env::var("AWS_LAMBDA_INITIALIZATION_TYPE").as_deref() == Ok("snap-start") +} - // Setup Amazon's default tracing data - amzn_trace_env(&invocation.context); +/// Runs the SnapStart restore lifecycle when SnapStart is enabled, and is a +/// no-op otherwise. +/// +/// The sequence is: run `before_snapshot` hooks (LIFO) → call `/restore/next` +/// (blocks until the VM is restored) → reset the internal RAPID connection pool +/// → run `after_restore` hooks (FIFO). A failure in the before-snapshot phase +/// (a hook or `/restore/next`) is reported to Lambda via `/init/error`; an +/// `after_restore` failure is reported via `/restore/error`. Either way the +/// error is returned so the runtime exits cleanly (no `process::exit`, so +/// graceful-shutdown handlers still run). +/// +/// This is a free function taking only the fields it needs (rather than `&self`) +/// so it does not borrow the handler `S` across an `.await`, which would require +/// `Runtime: Sync` in the by-value `run`/`run_concurrent` methods. +async fn snapstart_lifecycle( + client: &Arc, + resources: &[Arc], +) -> Result<(), BoxError> { + if !is_snapstart_env() { + return Ok(()); + } + run_restore_lifecycle(client, resources).await +} - // Wait for service to be ready - let ready = service.ready().await?; +/// Performs the restore lifecycle unconditionally (the env gate lives in the +/// caller, [`snapstart_lifecycle`]). Split out so it can be tested directly +/// without mutating the process-global `AWS_LAMBDA_INITIALIZATION_TYPE`. +async fn run_restore_lifecycle( + client: &Arc, + resources: &[Arc], +) -> Result<(), BoxError> { + // before_snapshot runs in REVERSE registration order (LIFO): tear down + // dependents before their foundations. A failure here happens during init, + // so report it to /init/error. + for resource in resources.iter().rev() { + if let Err(e) = resource.before_snapshot().await { + return Err(report_init_error(client, e).await); + } + } - // Once ready, call the service which will respond to the Lambda runtime API - ready.call(invocation).await?; + // Signal init complete and block until the VM is restored from snapshot. + let req = RestoreNextRequest.into_req()?; + let resp = match client.call(req).await { + Ok(resp) => resp, + // A transport failure calling /restore/next is still an init-phase error. + Err(e) => return Err(report_init_error(client, e).await), + }; + // `call()` only surfaces transport errors; a non-2xx status from RAPID still + // resolves to `Ok`. Treat a failed `/restore/next` as fatal rather than + // silently proceeding into the invocation loop with an un-restored VM. This + // still happens during init, so report it to /init/error. + let status = resp.status(); + if !status.is_success() { + let err: BoxError = format!("/restore/next returned a non-success status: {status}").into(); + return Err(report_init_error(client, err).await); + } + // Stale connections to RAPID won't survive the snapshot; rebuild the pool. + client.reset_pool(); + + // after_restore runs in registration order (FIFO): rebuild foundations + // before the dependents that need them. A failure here is reported to + // /restore/error and propagated so the runtime exits. + for resource in resources { + if let Err(e) = resource.after_restore().await { + return Err(report_restore_error(client, e).await); } - Ok(()) } + + Ok(()) } -/* ------------------------------------------- UTILS ------------------------------------------- */ +/// Reports an init-phase error (a `before_snapshot` hook or `/restore/next`) to +/// Lambda via `/init/error`, then returns the original error so the caller can +/// propagate it and exit the runtime. +async fn report_init_error(client: &Arc, err: BoxError) -> BoxError { + report_diagnostic(client, err, ReportKind::Init).await +} + +/// Reports an after-restore error to Lambda via `/restore/error`, then returns +/// the original error so the caller can propagate it and exit the runtime. +async fn report_restore_error(client: &Arc, err: BoxError) -> BoxError { + report_diagnostic(client, err, ReportKind::Restore).await +} + +enum ReportKind { + Init, + Restore, +} + +/// Posts a `Diagnostic` (built from the error) to the appropriate RAPID error +/// endpoint, then returns the **original** `BoxError` so the caller propagates it +/// with its concrete type and `source()` chain intact. Failures to build or send +/// the report are logged (never swallowed silently) but do not mask the error. +async fn report_diagnostic(client: &Arc, err: BoxError, kind: ReportKind) -> BoxError { + // Build the diagnostic from a reference so we can return `err` untouched. + let diagnostic = Diagnostic { + error_type: crate::diagnostic::type_name_of_val(&err), + error_message: err.to_string(), + }; + + let (endpoint, req) = match kind { + ReportKind::Init => ("/init/error", InitErrorRequest { diagnostic }.into_req()), + ReportKind::Restore => ("/restore/error", RestoreErrorRequest { diagnostic }.into_req()), + }; + + match req { + Ok(req) => { + if let Err(e) = client.call(req).await { + log_or_print!( + tracing: tracing::error!(error = ?e, "failed to report SnapStart error to {endpoint}"), + fallback: eprintln!("failed to report SnapStart error to {endpoint}: {e:?}") + ); + } + } + Err(e) => { + log_or_print!( + tracing: tracing::error!(error = ?e, "failed to build SnapStart {endpoint} request"), + fallback: eprintln!("failed to build SnapStart {endpoint} request: {e:?}") + ); + } + } + + err +} #[allow(clippy::type_complexity)] fn wrap_handler<'a, F, EventPayload, Response, BufferedResponse, StreamingResponse, StreamItem, StreamError>( @@ -203,6 +594,7 @@ fn wrap_handler<'a, F, EventPayload, Response, BufferedResponse, StreamingRespon StreamItem, StreamError, >, + ApiClient, > where F: Service, Response = Response>, @@ -233,6 +625,85 @@ fn incoming( } } +/// Creates a future that polls the `/next` endpoint. +#[cfg(feature = "concurrency-tokio")] +async fn next_event_future(client: &ApiClient) -> Result, BoxError> { + let req = NextEventRequest.into_req()?; + client.call(req).await +} + +fn max_concurrency_from_env() -> Option { + env::var("AWS_LAMBDA_MAX_CONCURRENCY") + .ok() + .and_then(|v| v.parse::().ok()) + .filter(|&c| c > 0) +} + +fn concurrency_env_value() -> Option { + env::var("AWS_LAMBDA_MAX_CONCURRENCY").ok() +} + +#[cfg(feature = "concurrency-tokio")] +async fn concurrent_worker_loop(mut service: S, config: Arc, client: Arc) -> Result<(), BoxError> +where + S: Service, + S::Future: Send, +{ + let task_id = tokio::task::id(); + let span = info_span!("worker", task_id = %task_id); + loop { + let event = match next_event_future(client.as_ref()).instrument(span.clone()).await { + Ok(event) => event, + Err(e) => { + warn!(task_id = %task_id, error = %e, "Error polling /next, retrying"); + continue; + } + }; + + process_invocation(&mut service, &config, event, false) + .instrument(span.clone()) + .await?; + } +} + +async fn process_invocation( + service: &mut S, + config: &Arc, + event: http::Response, + set_amzn_trace_env: bool, +) -> Result<(), BoxError> +where + S: Service, +{ + let (parts, incoming) = event.into_parts(); + + #[cfg(debug_assertions)] + if parts.status == http::StatusCode::NO_CONTENT { + // Ignore the event if the status code is 204. + // This is a way to keep the runtime alive when + // there are no events pending to be processed. + return Ok(()); + } + + // Build the invocation such that it can be sent to the service right away + // when it is ready + let body = incoming.collect().await?.to_bytes(); + let context = Context::new(invoke_request_id(&parts.headers)?, config.clone(), &parts.headers)?; + let invocation = LambdaInvocation { parts, body, context }; + + if set_amzn_trace_env { + // Setup Amazon's default tracing data + amzn_trace_env(&invocation.context); + } + + // Wait for service to be ready + let ready = service.ready().await?; + + // Once ready, call the service which will respond to the Lambda runtime API + ready.call(invocation).await?; + Ok(()) +} + fn amzn_trace_env(ctx: &Context) { match &ctx.xray_trace_id { Some(trace_id) => env::set_var("_X_AMZN_TRACE_ID", trace_id), @@ -249,15 +720,29 @@ mod endpoint_tests { use super::{incoming, wrap_handler}; use crate::{ requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest}, - Config, Diagnostic, Error, Runtime, + BoxFuture, Config, Diagnostic, Error, Runtime, }; - use futures::future::BoxFuture; - use http::{HeaderValue, StatusCode}; - use http_body_util::BodyExt; + use bytes::Bytes; + use http::{HeaderValue, Method, Request, Response, StatusCode}; + use http_body_util::{BodyExt, Full}; use httpmock::prelude::*; - use lambda_runtime_api_client::Client; - use std::{env, sync::Arc}; + use hyper::{body::Incoming, service::service_fn}; + use hyper_util::{ + rt::{tokio::TokioIo, TokioExecutor}, + server::conn::auto::Builder as ServerBuilder, + }; + use lambda_runtime_api_client::PooledClient as Client; + use std::{ + convert::Infallible, + env, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, + }; + use tokio::{net::TcpListener, sync::Notify}; use tokio_stream::StreamExt; #[tokio::test] @@ -276,7 +761,7 @@ mod endpoint_tests { }); let base = server.base_url().parse().expect("Invalid mock server Uri"); - let client = Client::builder().with_endpoint(base).build()?; + let client = Client::builder().with_endpoint(base).build(); let req = NextEventRequest.into_req()?; let rsp = client.call(req).await.expect("Unable to send request"); @@ -309,9 +794,13 @@ mod endpoint_tests { }); let base = server.base_url().parse().expect("Invalid mock server Uri"); - let client = Client::builder().with_endpoint(base).build()?; + let client = Client::builder().with_endpoint(base).build(); - let req = EventCompletionRequest::new("156cb537-e2d4-11e8-9b34-d36013741fb9", "{}"); + let req = EventCompletionRequest::new( + "156cb537-e2d4-11e8-9b34-d36013741fb9", + Option::Some("invocation_id"), + "{}", + ); let req = req.into_req()?; let rsp = client.call(req).await?; @@ -339,10 +828,11 @@ mod endpoint_tests { }); let base = server.base_url().parse().expect("Invalid mock server Uri"); - let client = Client::builder().with_endpoint(base).build()?; + let client = Client::builder().with_endpoint(base).build(); let req = EventErrorRequest { request_id: "156cb537-e2d4-11e8-9b34-d36013741fb9", + invocation_id: Option::Some("invocation_id"), diagnostic, }; let req = req.into_req()?; @@ -369,13 +859,13 @@ mod endpoint_tests { }); let next_response = server.mock(|when, then| { when.method(POST) - .path(format!("/2018-06-01/runtime/invocation/{}/response", request_id)) + .path(format!("/2018-06-01/runtime/invocation/{request_id}/response")) .body("{}"); then.status(200).body(""); }); let base = server.base_url().parse().expect("Invalid mock server Uri"); - let client = Client::builder().with_endpoint(base).build()?; + let client = Client::builder().with_endpoint(base).build(); async fn func(event: crate::LambdaEvent) -> Result { let (event, _) = event.into_parts(); @@ -409,6 +899,8 @@ mod endpoint_tests { client: client.clone(), config: Arc::new(config), service: wrap_handler(f, client), + concurrency_limit: 1, + snapstart_resources: Vec::new(), }; let client = &runtime.client; let incoming = incoming(client).take(1); @@ -440,13 +932,13 @@ mod endpoint_tests { let next_response = server.mock(|when, then| { when.method(POST) - .path(format!("/2018-06-01/runtime/invocation/{}/error", request_id)) + .path(format!("/2018-06-01/runtime/invocation/{request_id}/error")) .header("lambda-runtime-function-error-type", "unhandled"); then.status(200).body(""); }); let base = server.base_url().parse().expect("Invalid mock server Uri"); - let client = Client::builder().with_endpoint(base).build()?; + let client = Client::builder().with_endpoint(base).build(); let f = crate::service_fn(func); @@ -463,6 +955,8 @@ mod endpoint_tests { client: client.clone(), config, service: wrap_handler(f, client), + concurrency_limit: 1, + snapstart_resources: Vec::new(), }; let client = &runtime.client; let incoming = incoming(client).take(1); @@ -485,4 +979,597 @@ mod endpoint_tests { }) .await } + + #[cfg(feature = "concurrency-tokio")] + #[tokio::test] + async fn concurrent_worker_crash_does_not_stop_other_workers() -> Result<(), Error> { + let next_calls = Arc::new(AtomicUsize::new(0)); + let response_calls = Arc::new(AtomicUsize::new(0)); + let first_error_served = Arc::new(Notify::new()); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let base: http::Uri = format!("http://{addr}").parse().unwrap(); + + let server_handle = { + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + tokio::spawn(async move { + loop { + let (tcp, _) = match listener.accept().await { + Ok(v) => v, + Err(_) => return, + }; + + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + let service = service_fn(move |req: Request| { + let next_calls = next_calls.clone(); + let response_calls = response_calls.clone(); + let first_error_served = first_error_served.clone(); + async move { + let (parts, body) = req.into_parts(); + let method = parts.method; + let path = parts.uri.path().to_string(); + + if method == Method::POST { + // Drain request body to support keep-alive. + let _ = body.collect().await; + } + + if method == Method::GET && path == "/2018-06-01/runtime/invocation/next" { + let call_index = next_calls.fetch_add(1, Ordering::SeqCst); + match call_index { + // First worker errors (missing request id header). + 0 => { + first_error_served.notify_one(); + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + // Second worker should keep running and process an invocation, even if another worker errors. + 1 => { + first_error_served.notified().await; + let res = Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .header("lambda-runtime-aws-request-id", "good-request") + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + // Finally, error the remaining worker so the runtime can terminate and the test can assert behavior. + 2 => { + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-deadline-ms", "1542409706888") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } + _ => { + let res = Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + } + } + + if method == Method::POST && path.ends_with("/response") { + response_calls.fetch_add(1, Ordering::SeqCst); + let res = Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + + let res = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::new())) + .unwrap(); + Ok::<_, Infallible>(res) + } + }); + + let io = TokioIo::new(tcp); + tokio::spawn(async move { + if let Err(err) = ServerBuilder::new(TokioExecutor::new()) + .serve_connection(io, service) + .await + { + eprintln!("Error serving connection: {err:?}"); + } + }); + } + }) + }; + + async fn func(event: crate::LambdaEvent) -> Result { + Ok(event.payload) + } + + let handler = crate::service_fn(func); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + let runtime = Runtime { + client: client.clone(), + config: Arc::new(Config { + function_name: "test_fn".to_string(), + memory: 128, + version: "1".to_string(), + log_stream: "test_stream".to_string(), + log_group: "test_log".to_string(), + }), + service: wrap_handler(handler, client), + concurrency_limit: 2, + snapstart_resources: Vec::new(), + }; + + let res = tokio::time::timeout(Duration::from_secs(2), runtime.run_concurrent()).await; + assert!(res.is_ok(), "run_concurrent timed out"); + assert!( + res.unwrap().is_err(), + "expected runtime to terminate once all workers crashed" + ); + + assert_eq!( + response_calls.load(Ordering::SeqCst), + 1, + "expected remaining worker to keep running after a worker crash" + ); + + server_handle.abort(); + Ok(()) + } + + #[cfg(feature = "concurrency-tokio")] + // Must be current-thread (the default) so the thread-local tracing + // subscriber set via `set_default` propagates to spawned tasks. + #[tokio::test] + async fn test_concurrent_structured_logging_isolation() -> Result<(), Error> { + use std::collections::HashSet; + use tracing::info; + use tracing_capture::{CaptureLayer, SharedStorage}; + use tracing_subscriber::layer::SubscriberExt; + + let storage = SharedStorage::default(); + let subscriber = tracing_subscriber::registry().with(CaptureLayer::new(&storage)); + let _guard = tracing::subscriber::set_default(subscriber); + + let request_count = Arc::new(AtomicUsize::new(0)); + let done = Arc::new(tokio::sync::Notify::new()); + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + let base: http::Uri = format!("http://{addr}").parse()?; + + let server_handle = { + let request_count = request_count.clone(); + let done = done.clone(); + tokio::spawn(async move { + loop { + let (tcp, _) = match listener.accept().await { + Ok(v) => v, + Err(_) => return, + }; + + let request_count = request_count.clone(); + let done = done.clone(); + let service = service_fn(move |req: Request| { + let request_count = request_count.clone(); + let done = done.clone(); + async move { + let (parts, body) = req.into_parts(); + if parts.method == Method::POST { + let _ = body.collect().await; + } + + if parts.method == Method::GET && parts.uri.path() == "/2018-06-01/runtime/invocation/next" + { + let count = request_count.fetch_add(1, Ordering::SeqCst); + if count < 300 { + let request_id = format!("test-request-{}", count + 1); + let res = Response::builder() + .status(StatusCode::OK) + .header("lambda-runtime-aws-request-id", &request_id) + .header("lambda-runtime-deadline-ms", "9999999999999") + .body(Full::new(Bytes::from_static(b"{}"))) + .unwrap(); + return Ok::<_, Infallible>(res); + } else { + done.notify_one(); + let res = Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + } + + if parts.method == Method::POST && parts.uri.path().contains("/response") { + let res = Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::new())) + .unwrap(); + return Ok::<_, Infallible>(res); + } + + let res = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Full::new(Bytes::new())) + .unwrap(); + Ok::<_, Infallible>(res) + } + }); + + let io = TokioIo::new(tcp); + tokio::spawn(async move { + let _ = ServerBuilder::new(TokioExecutor::new()) + .serve_connection(io, service) + .await; + }); + } + }) + }; + + async fn test_handler(event: crate::LambdaEvent) -> Result<(), Error> { + let request_id = &event.context.request_id; + info!(observed_request_id = request_id); + tokio::time::sleep(Duration::from_millis(100)).await; + Ok(()) + } + + let handler = crate::service_fn(test_handler); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + // Add tracing layer to capture span fields + use crate::layers::trace::TracingLayer; + use tower::ServiceBuilder; + let service = ServiceBuilder::new() + .layer(TracingLayer::new()) + .service(wrap_handler(handler, client.clone())); + + let runtime = Runtime { + client: client.clone(), + config: Arc::new(Config { + function_name: "test_fn".to_string(), + memory: 128, + version: "1".to_string(), + log_stream: "test_stream".to_string(), + log_group: "test_log".to_string(), + }), + service, + concurrency_limit: 3, + snapstart_resources: Vec::new(), + }; + + let runtime_handle = tokio::spawn(async move { runtime.run_concurrent().await }); + + done.notified().await; + // Give handlers time to complete after server signals done + tokio::time::sleep(Duration::from_millis(500)).await; + + runtime_handle.abort(); + server_handle.abort(); + + let storage = storage.lock(); + let events: Vec<_> = storage + .all_events() + .filter(|e| e.value("observed_request_id").is_some()) + .collect(); + + assert!( + events.len() >= 300, + "Should have at least 300 log entries, got {}", + events.len() + ); + + let mut seen_ids = HashSet::new(); + for event in &events { + let observed_id = event["observed_request_id"].as_str().unwrap(); + + // Find the parent "Lambda runtime invoke" span and get its requestId + let span_request_id = event + .ancestors() + .find(|s| s.metadata().name() == "Lambda runtime invoke") + .and_then(|s| s.value("requestId")) + .and_then(|v| v.as_str()) + .expect("Event should have a Lambda runtime invoke ancestor with requestId"); + + assert!( + observed_id.starts_with("test-request-"), + "Request ID should match pattern: {}", + observed_id + ); + assert!( + seen_ids.insert(observed_id.to_string()), + "Request ID should be unique: {}", + observed_id + ); + + // Verify span request ID matches logged request ID + assert_eq!( + observed_id, span_request_id, + "Span request ID should match logged request ID: span={}, logged={}", + span_request_id, observed_id + ); + } + + Ok(()) + } + + /// Records the order in which `before_snapshot`/`after_restore` fire across + /// resources, so tests can assert LIFO/FIFO ordering. + struct OrderRecorder { + label: &'static str, + log: Arc>>, + } + + impl crate::SnapStartResource for OrderRecorder { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + let log = self.log.clone(); + let label = self.label; + Box::pin(async move { + log.lock().unwrap().push(format!("before:{label}")); + Ok(()) + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + let log = self.log.clone(); + let label = self.label; + Box::pin(async move { + log.lock().unwrap().push(format!("after:{label}")); + Ok(()) + }) + } + } + + /// Which lifecycle phase a [`FailingResource`] should fail in. + #[derive(Clone, Copy)] + enum Phase { + BeforeSnapshot, + AfterRestore, + } + + /// A resource that returns an error from the selected phase (and is a no-op + /// in the other), used to exercise the error-reporting paths. + struct FailingResource { + phase: Phase, + } + + impl crate::SnapStartResource for FailingResource { + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + let fail = matches!(self.phase, Phase::BeforeSnapshot); + Box::pin(async move { + if fail { + Err("before_snapshot failed".into()) + } else { + Ok(()) + } + }) + } + + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + let fail = matches!(self.phase, Phase::AfterRestore); + Box::pin(async move { + if fail { + Err("after_restore failed".into()) + } else { + Ok(()) + } + }) + } + } + + #[tokio::test] + async fn test_snapstart_restore_lifecycle_calls_restore_next() -> Result<(), Error> { + let server = MockServer::start(); + + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + // Exercise the restore lifecycle directly (no env mutation, no races with + // other tests that call run()/run_concurrent()). + super::run_restore_lifecycle(&client, &resources).await?; + + restore_mock.assert_async().await; + let recorded = log.lock().unwrap().clone(); + assert_eq!(recorded, vec!["before:pool", "after:pool"]); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_resource_ordering_is_lifo_then_fifo() -> Result<(), Error> { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + // Registration order: credentials → pool → cache (foundations first). + let resources: Vec> = vec![ + Arc::new(OrderRecorder { + label: "credentials", + log: log.clone(), + }), + Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + }), + Arc::new(OrderRecorder { + label: "cache", + log: log.clone(), + }), + ]; + + super::run_restore_lifecycle(&client, &resources).await?; + + let recorded = log.lock().unwrap().clone(); + assert_eq!( + recorded, + vec![ + // before_snapshot: LIFO (reverse registration) + "before:cache", + "before:pool", + "before:credentials", + // after_restore: FIFO (registration order) + "after:credentials", + "after:pool", + "after:cache", + ] + ); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_restore_next_non_success_is_fatal() -> Result<(), Error> { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(500).body(""); + }); + let init_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/init/error"); + then.status(202).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + // A non-2xx /restore/next must surface as an error rather than silently + // continuing into the invocation loop with an un-restored VM, and it must + // be reported to /init/error. + assert!(result.is_err(), "expected non-success /restore/next to be fatal"); + init_err_mock.assert_async().await; + // before_snapshot ran; after_restore must NOT run when the restore call fails. + let recorded = log.lock().unwrap().clone(); + assert_eq!(recorded, vec!["before:pool"]); + Ok(()) + } + + #[tokio::test] + async fn test_snapstart_lifecycle_is_noop_when_not_snapstart() -> Result<(), Error> { + // Only meaningful when the SnapStart env var is absent. Never mutate the + // global env (would race with parallel tests); skip if it's set. + if env::var("AWS_LAMBDA_INITIALIZATION_TYPE").is_ok() { + return Ok(()); + } + + let server = MockServer::start(); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let log = Arc::new(std::sync::Mutex::new(Vec::new())); + let resources: Vec> = vec![Arc::new(OrderRecorder { + label: "pool", + log: log.clone(), + })]; + + // The env-gated entry point must do nothing when SnapStart is not enabled. + super::snapstart_lifecycle(&client, &resources).await?; + + assert_eq!( + restore_mock.calls(), + 0, + "/restore/next must not be called when not snap-start" + ); + assert!( + log.lock().unwrap().is_empty(), + "no hooks should fire when not snap-start" + ); + Ok(()) + } + + #[tokio::test] + async fn test_before_snapshot_failure_reports_init_error() -> Result<(), Error> { + let server = MockServer::start(); + let init_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/init/error"); + then.status(202).body(""); + }); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let resources: Vec> = vec![Arc::new(FailingResource { + phase: Phase::BeforeSnapshot, + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + assert!(result.is_err(), "before_snapshot failure must propagate"); + init_err_mock.assert_async().await; + // /restore/next must NOT be reached when before_snapshot fails. + assert_eq!(restore_mock.calls(), 0); + Ok(()) + } + + #[tokio::test] + async fn test_after_restore_failure_reports_restore_error() -> Result<(), Error> { + let server = MockServer::start(); + let restore_mock = server.mock(|when, then| { + when.method(GET).path("/2018-06-01/runtime/restore/next"); + then.status(200).body(""); + }); + let restore_err_mock = server.mock(|when, then| { + when.method(POST).path("/2018-06-01/runtime/restore/error"); + then.status(202).body(""); + }); + + let base = server.base_url().parse().expect("Invalid mock server Uri"); + let client = Arc::new(Client::builder().with_endpoint(base).build()); + + let resources: Vec> = vec![Arc::new(FailingResource { + phase: Phase::AfterRestore, + })]; + + let result = super::run_restore_lifecycle(&client, &resources).await; + + // Now that the runtime no longer calls process::exit, the after_restore + // failure path returns an error and is unit-testable. + assert!(result.is_err(), "after_restore failure must propagate"); + restore_mock.assert_async().await; + restore_err_mock.assert_async().await; + Ok(()) + } } diff --git a/lambda-runtime/src/snapstart.rs b/lambda-runtime/src/snapstart.rs new file mode 100644 index 000000000..c9d81b11d --- /dev/null +++ b/lambda-runtime/src/snapstart.rs @@ -0,0 +1,104 @@ +//! SnapStart lifecycle support. +//! +//! AWS Lambda SnapStart reduces cold start latency by taking a Firecracker +//! microVM snapshot of the initialized execution environment and restoring from +//! it on subsequent invocations. Resources initialized before the snapshot may +//! become invalid after restore: TCP connections are dropped, credentials may +//! expire, and unique values generated during init would be shared across every +//! restored instance. +//! +//! Implement [`SnapStartResource`] on types that hold such resources and +//! register them with +//! [`Runtime::register_snapstart_resource`](crate::Runtime::register_snapstart_resource). +//! The runtime invokes the registered hooks around the snapshot/restore boundary +//! when SnapStart is enabled (`AWS_LAMBDA_INITIALIZATION_TYPE == "snap-start"`) +//! and does nothing otherwise. +//! +//! # Ordering +//! +//! Resources are orchestrated with stack (LIFO/FIFO) ordering, mirroring how +//! Go's `defer`, middleware unwinding, and Rust destructors work: +//! +//! - [`before_snapshot`](SnapStartResource::before_snapshot) runs in **reverse** +//! registration order (LIFO) — dependents tear down before their foundations. +//! - [`after_restore`](SnapStartResource::after_restore) runs in **registration** +//! order (FIFO) — foundations rebuild before the dependents that need them. +//! +//! The rule for users is one line: **register foundations first** (e.g. +//! credentials before the pool that uses them, the pool before the cache that +//! uses it). Teardown and rebuild then both happen in the correct relative order +//! automatically: +//! +//! ```text +//! register: credentials → pool → cache +//! before_snapshot: cache → pool → credentials (reverse) +//! after_restore: credentials → pool → cache (forward) +//! ``` +//! +//! # Example +//! +//! ```no_run +//! use lambda_runtime::{BoxFuture, Error, SnapStartResource}; +//! +//! struct DbPool { +//! // e.g. a connection pool handle +//! } +//! +//! impl SnapStartResource for DbPool { +//! fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { +//! Box::pin(async move { +//! // release connections before the snapshot +//! Ok(()) +//! }) +//! } +//! +//! fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { +//! Box::pin(async move { +//! // re-establish connections after restore +//! Ok(()) +//! }) +//! } +//! } +//! ``` + +use crate::{BoxFuture, Error}; + +/// A resource that needs custom logic around the SnapStart snapshot/restore +/// boundary. +/// +/// Implement this trait on types holding snapshot-sensitive state (connection +/// pools, credentials, cached DNS, etc.) and register them with +/// [`Runtime::register_snapstart_resource`](crate::Runtime::register_snapstart_resource). +/// +/// Both methods default to a no-op, so implementors only override the hook they +/// need. See the [module docs](crate::snapstart) for ordering semantics. +/// +/// Each method returns a [`BoxFuture`] (rather than being an `async fn`) so the +/// trait stays object-safe and can be stored as `dyn SnapStartResource` without +/// pulling in the `async-trait` crate. Implementations wrap their body in +/// `Box::pin(async move { .. })`. +pub trait SnapStartResource: Send + Sync { + /// Called before the VM snapshot is taken. + /// + /// Use this to release resources that will not survive the snapshot, such as + /// open connections or buffered data. + /// + /// Runs in reverse registration order (LIFO) across all registered + /// resources. + fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async { Ok(()) }) + } + + /// Called after the VM is restored from a snapshot. + /// + /// Use this to re-establish resources released in + /// [`before_snapshot`](Self::before_snapshot), refresh credentials, or + /// regenerate values that must be unique per execution environment. + /// + /// Runs in registration order (FIFO) across all registered resources. If + /// this returns an error, the runtime reports it to Lambda via + /// `/restore/error` and the runtime exits. + fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> { + Box::pin(async { Ok(()) }) + } +} diff --git a/lambda-runtime/src/types.rs b/lambda-runtime/src/types.rs index ee09978f7..edd932402 100644 --- a/lambda-runtime/src/types.rs +++ b/lambda-runtime/src/types.rs @@ -1,4 +1,11 @@ -use crate::{Error, RefConfig}; +use crate::{ + constants::{ + LAMBDA_RUNTIME_CLIENT_CONTEXT, LAMBDA_RUNTIME_COGNITO_IDENTITY, LAMBDA_RUNTIME_DEADLINE_MS, + LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN, LAMBDA_RUNTIME_REQUEST_ID, LAMBDA_RUNTIME_TENANT_ID, + LAMBDA_RUNTIME_TRACE_ID, + }, + Error, RefConfig, +}; use base64::prelude::*; use bytes::Bytes; use http::{header::ToStrError, HeaderMap, HeaderValue, StatusCode}; @@ -79,6 +86,8 @@ pub struct Context { /// unless the invocation request to the Lambda APIs was made using AWS /// credentials issues by Amazon Cognito Identity Pools. pub identity: Option, + /// The tenant ID for the current invocation. + pub tenant_id: Option, /// Lambda function configuration from the local environment variables. /// Includes information such as the function name, memory allocation, /// version, and log streams. @@ -94,6 +103,7 @@ impl Default for Context { xray_trace_id: None, client_context: None, identity: None, + tenant_id: None, env_config: std::sync::Arc::new(crate::Config::default()), } } @@ -103,14 +113,24 @@ impl Context { /// Create a new [Context] struct based on the function configuration /// and the incoming request data. pub fn new(request_id: &str, env_config: RefConfig, headers: &HeaderMap) -> Result { - let client_context: Option = if let Some(value) = headers.get("lambda-runtime-client-context") { - serde_json::from_str(value.to_str()?)? + let client_context: Option = if let Some(value) = headers.get(LAMBDA_RUNTIME_CLIENT_CONTEXT) { + let raw = value.to_str()?; + if raw.is_empty() { + None + } else { + Some(serde_json::from_str(raw)?) + } } else { None }; - let identity: Option = if let Some(value) = headers.get("lambda-runtime-cognito-identity") { - serde_json::from_str(value.to_str()?)? + let identity: Option = if let Some(value) = headers.get(LAMBDA_RUNTIME_COGNITO_IDENTITY) { + let raw = value.to_str()?; + if raw.is_empty() { + None + } else { + Some(serde_json::from_str(raw)?) + } } else { None }; @@ -118,22 +138,25 @@ impl Context { let ctx = Context { request_id: request_id.to_owned(), deadline: headers - .get("lambda-runtime-deadline-ms") + .get(LAMBDA_RUNTIME_DEADLINE_MS) .expect("missing lambda-runtime-deadline-ms header") .to_str()? .parse::()?, invoked_function_arn: headers - .get("lambda-runtime-invoked-function-arn") + .get(LAMBDA_RUNTIME_INVOKED_FUNCTION_ARN) .unwrap_or(&HeaderValue::from_static( "No header lambda-runtime-invoked-function-arn found.", )) .to_str()? .to_owned(), xray_trace_id: headers - .get("lambda-runtime-trace-id") + .get(LAMBDA_RUNTIME_TRACE_ID) .map(|v| String::from_utf8_lossy(v.as_bytes()).to_string()), client_context, identity, + tenant_id: headers + .get(LAMBDA_RUNTIME_TENANT_ID) + .map(|v| String::from_utf8_lossy(v.as_bytes()).to_string()), env_config, }; @@ -149,7 +172,7 @@ impl Context { /// Extract the invocation request id from the incoming request. pub(crate) fn invoke_request_id(headers: &HeaderMap) -> Result<&str, ToStrError> { headers - .get("lambda-runtime-aws-request-id") + .get(LAMBDA_RUNTIME_REQUEST_ID) .expect("missing lambda-runtime-aws-request-id header") .to_str() } @@ -176,7 +199,7 @@ impl LambdaEvent { } /// Metadata prelude for a stream response. -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct MetadataPrelude { #[serde(with = "http_serde::status_code")] @@ -275,6 +298,7 @@ where #[cfg(test)] mod test { + use super::*; use crate::Config; use std::sync::Arc; @@ -478,4 +502,45 @@ mod test { let _ = invoke_request_id(&headers); } + + #[test] + fn serde_metadata_prelude() { + let metadata_prelude = MetadataPrelude { + status_code: StatusCode::OK, + headers: { + let mut headers = HeaderMap::new(); + headers.insert("key", "val".parse().unwrap()); + headers + }, + cookies: vec!["cookie".to_string()], + }; + + let serialized = serde_json::to_string(&metadata_prelude).unwrap(); + let deserialized: MetadataPrelude = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(metadata_prelude, deserialized); + } + + #[test] + fn context_with_tenant_id_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); + headers.insert("lambda-runtime-aws-tenant-id", HeaderValue::from_static("tenant-123")); + + let context = Context::new("id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, Some("tenant-123".to_string())); + } + + #[test] + fn context_without_tenant_id_resolves() { + let config = Arc::new(Config::default()); + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", HeaderValue::from_static("my-id")); + headers.insert("lambda-runtime-deadline-ms", HeaderValue::from_static("123")); + + let context = Context::new("id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, None); + } } diff --git a/lambda-runtime/tests/tenant_id_integration.rs b/lambda-runtime/tests/tenant_id_integration.rs new file mode 100644 index 000000000..39d65ccf4 --- /dev/null +++ b/lambda-runtime/tests/tenant_id_integration.rs @@ -0,0 +1,93 @@ +use http::HeaderMap; +use lambda_runtime::Context; +use std::sync::Arc; + +#[test] +fn test_context_tenant_id_extraction() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert("lambda-runtime-aws-tenant-id", "my-tenant-123".parse().unwrap()); + + let context = Context::new("test-id", config.clone(), &headers).unwrap(); + assert_eq!(context.tenant_id, Some("my-tenant-123".to_string())); + + // Test without tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, None); +} + +#[test] +fn test_context_tenant_id_with_special_characters() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with tenant ID containing special characters + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert( + "lambda-runtime-aws-tenant-id", + "tenant-with-dashes_and_underscores.123".parse().unwrap(), + ); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!( + context.tenant_id, + Some("tenant-with-dashes_and_underscores.123".to_string()) + ); +} + +#[test] +fn test_context_tenant_id_empty_value() { + let config = Arc::new(lambda_runtime::Config::default()); + + // Test with empty tenant ID + let mut headers = HeaderMap::new(); + headers.insert("lambda-runtime-aws-request-id", "test-id".parse().unwrap()); + headers.insert("lambda-runtime-deadline-ms", "123456789".parse().unwrap()); + headers.insert("lambda-runtime-aws-tenant-id", "".parse().unwrap()); + + let context = Context::new("test-id", config, &headers).unwrap(); + assert_eq!(context.tenant_id, Some("".to_string())); +} + +#[test] +fn test_trace_layer_request_span_creation() { + use lambda_runtime::layers::trace::request_span; + + // Test with both trace ID and tenant ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.xray_trace_id = Some("trace-123".to_string()); + context.tenant_id = Some("tenant-456".to_string()); + + let _span = request_span(&context); + // Just verify the span can be created without panicking + + // Test with only trace ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.xray_trace_id = Some("trace-123".to_string()); + + let _span = request_span(&context); + + // Test with only tenant ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + context.tenant_id = Some("tenant-only".to_string()); + + let _span = request_span(&context); + + // Test with only request ID + let mut context = Context::default(); + context.request_id = "test-request".to_string(); + + let _span = request_span(&context); +} diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 000000000..67e95dca5 --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,17 @@ +[workspace] +semver_check = true +git_release_enable = false + +[[package]] +name = "lambda_runtime" +git_release_enable = true +git_release_name = "v{{ version }}" +changelog_include = ["lambda_http", "lambda_runtime_api_client", "lambda-extension", "aws_lambda_events"] + +[[package]] +name = "lambda-integration-tests" +publish = false +release = false + +[changelog] +protect_breaking_commits = true diff --git a/scripts/build-examples.sh b/scripts/build-examples.sh new file mode 100755 index 000000000..2e577c3a0 --- /dev/null +++ b/scripts/build-examples.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +OUTPUT_DIR="${OUTPUT_DIR:-/tmp/var-task}" +HANDLERS_TO_BUILD="${HANDLERS_TO_BUILD:-}" + +mkdir -p "$OUTPUT_DIR" + +echo "Building handlers: ${HANDLERS_TO_BUILD}" + + +for handler in ${HANDLERS_TO_BUILD}; do + dir="examples/$handler" + if [ ! -f "$dir/Cargo.toml" ]; then + echo "✗ $handler not found" + continue + fi + + echo "Building $handler..." + if ! (cd "$dir" && cargo build --release); then + continue + fi + + if [ ! -f "$dir/target/release/$handler" ]; then + echo "✗ $handler artifact not found" + continue + fi + + cp "$dir/target/release/$handler" "$OUTPUT_DIR/" + echo "✓ $handler" +done + +echo "" +ls -lh "$OUTPUT_DIR/" 2>/dev/null || echo "No binaries built" +exit 0 diff --git a/scripts/custom-lambda-entrypoint.sh b/scripts/custom-lambda-entrypoint.sh new file mode 100644 index 000000000..fb02b26c5 --- /dev/null +++ b/scripts/custom-lambda-entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# custom entrypoint script to allow selection of multiple rust binaries for dockerized tests +set -e + +HANDLER=${1:-basic-lambda} + +if [ -f "/var/task/$HANDLER" ]; then + ln -sf "/var/task/$HANDLER" "${LAMBDA_RUNTIME_DIR}/bootstrap" + exec /usr/local/bin/aws-lambda-rie "${LAMBDA_RUNTIME_DIR}/bootstrap" +else + echo "Error: Handler '$HANDLER' not found in /var/task" + echo "Available handlers:" + ls -la /var/task + exit 1 +fi diff --git a/scripts/download-rie.sh b/scripts/download-rie.sh new file mode 100644 index 000000000..0d22509aa --- /dev/null +++ b/scripts/download-rie.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +set -eu + +if [ "$#" -ne 5 ]; then + echo "Usage: $0 TARGETARCH RIE_VERSION RIE_SHA256_AMD64 RIE_SHA256_ARM64 RIE_PATH" >&2 + exit 1 +fi + +TARGETARCH=$1 +RIE_VERSION=$2 +RIE_SHA256_AMD64=$3 +RIE_SHA256_ARM64=$4 +RIE_PATH=$5 + +case "${TARGETARCH}" in + amd64) + RIE_ASSET=aws-lambda-rie + RIE_SHA256=${RIE_SHA256_AMD64} + ;; + arm64) + RIE_ASSET=aws-lambda-rie-arm64 + RIE_SHA256=${RIE_SHA256_ARM64} + ;; + *) + echo "Unsupported target architecture: ${TARGETARCH}" >&2 + exit 1 + ;; +esac + +: "${RIE_PATH:?RIE_PATH must be set}" +RIE_TMP=$(mktemp) +trap 'rm -f "${RIE_TMP}"' EXIT + +curl \ + --fail \ + --location \ + --silent \ + --show-error \ + --retry 3 \ + --retry-all-errors \ + --output "${RIE_TMP}" \ + "https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v${RIE_VERSION}/${RIE_ASSET}" + +echo "${RIE_SHA256} ${RIE_TMP}" | sha256sum --check --status +install -m 0755 "${RIE_TMP}" "${RIE_PATH}" diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 000000000..ae40a9f1a --- /dev/null +++ b/scripts/pre-commit @@ -0,0 +1,25 @@ +#!/bin/sh +# Pre-commit hook to run cargo fmt +# +# To install this hook, run: +# cp scripts/pre-commit .git/hooks/pre-commit +# chmod +x .git/hooks/pre-commit + +echo "Running cargo fmt..." + +# Run cargo fmt on the entire workspace +cargo fmt --all -- --check + +# Check if cargo fmt found any formatting issues +if [ $? -ne 0 ]; then + echo "" + echo "❌ Code formatting issues detected!" + echo "Running 'cargo fmt --all' to fix formatting..." + cargo fmt --all + echo "" + echo "✅ Formatting applied. Please review the changes and commit again." + exit 1 +fi + +echo "✅ Code formatting is correct!" +exit 0 diff --git a/scripts/test-rie.sh b/scripts/test-rie.sh new file mode 100755 index 000000000..9de6feddd --- /dev/null +++ b/scripts/test-rie.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +EXAMPLE=${1:-basic-lambda} +# Optional: set RIE_MAX_CONCURRENCY to enable LMI mode (emulates AWS_LAMBDA_MAX_CONCURRENCY) +RIE_MAX_CONCURRENCY=${RIE_MAX_CONCURRENCY:-} + +echo "Building Docker image with RIE for example: $EXAMPLE..." +docker build -f Dockerfile.rie --build-arg EXAMPLE=$EXAMPLE -t rust-lambda-rie-test . + +echo "Starting RIE container on port 9000..." +if [ -n "$RIE_MAX_CONCURRENCY" ]; then + echo "Enabling LMI mode with AWS_LAMBDA_MAX_CONCURRENCY=$RIE_MAX_CONCURRENCY" + docker run -p 9000:8080 -e AWS_LAMBDA_MAX_CONCURRENCY="$RIE_MAX_CONCURRENCY" rust-lambda-rie-test & +else + docker run -p 9000:8080 rust-lambda-rie-test & +fi +CONTAINER_PID=$! + +echo "Container started. Test with:" +if [ "$EXAMPLE" = "basic-lambda" ] || [ "$EXAMPLE" = "basic-lambda-concurrent" ] || [ "$EXAMPLE" = "invocation-id-concurrent" ]; then + echo "curl -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' -d '{\"command\": \"test from RIE\"}' -H 'Content-Type: application/json'" +else + echo "For example '$EXAMPLE', check examples/$EXAMPLE/src/main.rs for the expected payload format." +fi +echo "" +echo "Press Ctrl+C to stop the container." + +wait $CONTAINER_PID \ No newline at end of file diff --git a/test/dockerized/scenarios/concurrent_scenarios.py b/test/dockerized/scenarios/concurrent_scenarios.py new file mode 100644 index 000000000..8370289b3 --- /dev/null +++ b/test/dockerized/scenarios/concurrent_scenarios.py @@ -0,0 +1,97 @@ +"""Multi-concurrency test scenarios.""" + +import os +from containerized_test_runner.models import ConcurrentTest, Request + +HANDLER = "basic-lambda-concurrent" +INVOCATION_ID_HANDLER = "invocation-id-concurrent" +IMAGE = os.environ.get("TEST_IMAGE", "local/test-base") +SAME_REQUEST_ID = "shared-request-id" +DEFAULT_CONCURRENCY = 10 +TIMEOUT = 5 + + +def _make_env(handler: str = HANDLER, concurrency: int = DEFAULT_CONCURRENCY) -> dict: + return { + "_HANDLER": handler, + "AWS_LAMBDA_MAX_CONCURRENCY": str(concurrency), + "AWS_LAMBDA_LOG_FORMAT": "JSON", + } + + +def _invocation_id_env( + handler: str = INVOCATION_ID_HANDLER, + concurrency: int = DEFAULT_CONCURRENCY, + timeout: int = TIMEOUT, +) -> dict: + return _make_env(handler, concurrency) | { + "AWS_LAMBDA_FUNCTION_TIMEOUT": str(timeout), + } + + +def get_concurrent_scenarios(): + scenarios = [] + + # Happy path: DEFAULT_CONCURRENCY unique commands all succeed concurrently + batch = [ + Request( + payload={"command": f"task-{i}"}, + assertions=[{"transform": "{msg: .msg}", "response": {"msg": f"Command task-{i} executed."}}], + ) + for i in range(DEFAULT_CONCURRENCY) + ] + scenarios.append(ConcurrentTest( + name="concurrent_happy_path", + handler=HANDLER, + environment_variables=_make_env(), + request_batches=[batch], + image=IMAGE, + )) + + # Error isolation: N-1 failing requests + 1 valid — the valid one must still succeed + mixed_batch = [ + Request( + payload={"command": "fail"}, + assertions=[{"errorType": "HandlerError"}], + ) + for _ in range(DEFAULT_CONCURRENCY - 1) + ] + [ + Request( + payload={"command": "survivor"}, + assertions=[{"transform": "{msg: .msg}", "response": {"msg": "Command survivor executed."}}], + ) + ] + scenarios.append(ConcurrentTest( + name="concurrent_error_isolation", + handler=HANDLER, + environment_variables=_make_env(), + request_batches=[mixed_batch], + image=IMAGE, + )) + + return scenarios + + +def get_invocation_id_scenarios(): + batches = [ + [Request.create( + payload={"command": "invoke-A", "sleep": TIMEOUT + 2}, + assertions=[{"transform": ".errorType", "error": "Sandbox.Timedout"}], + headers={"X-Amzn-RequestId": SAME_REQUEST_ID}, + )], + [Request.create( + payload={"command": "invoke-B", "sleep": TIMEOUT - 1}, + assertions=[{"response": {"from": "invoke-B"}}], + headers={"X-Amzn-RequestId": SAME_REQUEST_ID}, + )], + ] + + + return [ConcurrentTest( + name="invocation_id", + handler=INVOCATION_ID_HANDLER, + environment_variables=_invocation_id_env(handler=INVOCATION_ID_HANDLER, timeout=TIMEOUT), + request_batches=batches, + image=IMAGE, + )] + diff --git a/test/dockerized/suites/core.json b/test/dockerized/suites/core.json new file mode 100644 index 000000000..d5d56ce7d --- /dev/null +++ b/test/dockerized/suites/core.json @@ -0,0 +1,90 @@ +{ + "tests": [ + { + "name": "test_echo", + "handler": "basic-lambda", + "request": { + "command": "test" + }, + "assertions": [ + { + "response": { + "msg": "Command test executed." + }, + "transform": "{msg: .msg}" + } + ] + }, + { + "name": "test_basic_sqs", + "handler": "basic-sqs", + "request": { + "Records": [ + { + "messageId": "msg-001", + "receiptHandle": "receipt-001", + "body": "{\"id\":\"user-123\",\"text\":\"Hello from SQS\"}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1234567890", + "SenderId": "AIDAI123456789", + "ApproximateFirstReceiveTimestamp": "1234567890" + }, + "messageAttributes": {}, + "md5OfBody": "abc123", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:test-queue", + "awsRegion": "us-east-1" + } + ] + }, + "assertions": [ + { + "response": null + } + ] + }, + { + "name": "test_http_basic_lambda", + "handler": "http-basic-lambda", + "request": { + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "headers": { + "accept": "text/html", + "user-agent": "test-client" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "example.com", + "domainPrefix": "api", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "127.0.0.1", + "userAgent": "test-client" + }, + "requestId": "req-001", + "routeKey": "$default", + "stage": "$default", + "time": "01/Jan/2024:00:00:00 +0000", + "timeEpoch": 1704067200000 + }, + "isBase64Encoded": false + }, + "assertions": [ + { + "response": { + "statusCode": 200, + "body": "Hello AWS Lambda HTTP request" + }, + "transform": "{statusCode: .statusCode, body: .body}" + } + ] + } + ] +}