From eee1f21541e6411919710aef8ff14e65af52ffd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 24 Oct 2025 16:44:27 +0200 Subject: [PATCH 1/7] fix deployment issues --- .../{quoteapi => quoteapi-alb}/Dockerfile | 2 +- Examples/quoteapi-alb/Makefile | 63 ++++++++ Examples/quoteapi-alb/Package.swift | 43 +++++ Examples/quoteapi-alb/README.md | 104 ++++++++++++ .../Sources/QuoteAPI/QuoteService.swift | 79 ++++++++++ .../QuoteAPI/openapi-generator-config.yaml | 3 + .../Sources/QuoteAPI/openapi.yaml | 54 +++++++ Examples/quoteapi-alb/events/GetQuote.json | 25 +++ Examples/quoteapi-alb/template.yaml | 149 ++++++++++++++++++ .../{quoteapi => quoteapi-apigtw}/.gitignore | 0 Examples/quoteapi-apigtw/Dockerfile | 3 + .../{quoteapi => quoteapi-apigtw}/Makefile | 4 +- .../Package.swift | 0 .../{quoteapi => quoteapi-apigtw}/README.md | 0 .../Sources/LambdaAuthorizer/main.swift | 0 .../QuoteAPI/AuthenticateUserMiddleware.swift | 0 .../Sources/QuoteAPI/QuoteService.swift | 0 .../QuoteAPI/openapi-generator-config.yaml | 0 .../Sources/QuoteAPI/openapi.yaml | 0 .../events/404.json | 0 .../events/GetQuote.json | 0 .../events/HealthCheck.json | 0 .../template.yml | 0 Sources/ALB/ALBTargetGroup+HTTPRequest.swift | 45 ++++++ Sources/ALB/OpenAPILambdaALB.swift | 41 +++++ 25 files changed, 612 insertions(+), 3 deletions(-) rename Examples/{quoteapi => quoteapi-alb}/Dockerfile (60%) create mode 100644 Examples/quoteapi-alb/Makefile create mode 100644 Examples/quoteapi-alb/Package.swift create mode 100644 Examples/quoteapi-alb/README.md create mode 100644 Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift create mode 100644 Examples/quoteapi-alb/Sources/QuoteAPI/openapi-generator-config.yaml create mode 100644 Examples/quoteapi-alb/Sources/QuoteAPI/openapi.yaml create mode 100644 Examples/quoteapi-alb/events/GetQuote.json create mode 100644 Examples/quoteapi-alb/template.yaml rename Examples/{quoteapi => quoteapi-apigtw}/.gitignore (100%) create mode 100644 Examples/quoteapi-apigtw/Dockerfile rename Examples/{quoteapi => quoteapi-apigtw}/Makefile (91%) rename Examples/{quoteapi => quoteapi-apigtw}/Package.swift (100%) rename Examples/{quoteapi => quoteapi-apigtw}/README.md (100%) rename Examples/{quoteapi => quoteapi-apigtw}/Sources/LambdaAuthorizer/main.swift (100%) rename Examples/{quoteapi => quoteapi-apigtw}/Sources/QuoteAPI/AuthenticateUserMiddleware.swift (100%) rename Examples/{quoteapi => quoteapi-apigtw}/Sources/QuoteAPI/QuoteService.swift (100%) rename Examples/{quoteapi => quoteapi-apigtw}/Sources/QuoteAPI/openapi-generator-config.yaml (100%) rename Examples/{quoteapi => quoteapi-apigtw}/Sources/QuoteAPI/openapi.yaml (100%) rename Examples/{quoteapi => quoteapi-apigtw}/events/404.json (100%) rename Examples/{quoteapi => quoteapi-apigtw}/events/GetQuote.json (100%) rename Examples/{quoteapi => quoteapi-apigtw}/events/HealthCheck.json (100%) rename Examples/{quoteapi => quoteapi-apigtw}/template.yml (100%) create mode 100644 Sources/ALB/ALBTargetGroup+HTTPRequest.swift create mode 100644 Sources/ALB/OpenAPILambdaALB.swift diff --git a/Examples/quoteapi/Dockerfile b/Examples/quoteapi-alb/Dockerfile similarity index 60% rename from Examples/quoteapi/Dockerfile rename to Examples/quoteapi-alb/Dockerfile index e797a52..fbcbc27 100644 --- a/Examples/quoteapi/Dockerfile +++ b/Examples/quoteapi-alb/Dockerfile @@ -1,3 +1,3 @@ # image used to compile your Swift code -FROM public.ecr.aws/docker/library/swift:6.1-amazonlinux2 +FROM public.ecr.aws/docker/library/swift:6.2-amazonlinux2 RUN yum -y install git jq tar zip openssl-devel diff --git a/Examples/quoteapi-alb/Makefile b/Examples/quoteapi-alb/Makefile new file mode 100644 index 0000000..f186709 --- /dev/null +++ b/Examples/quoteapi-alb/Makefile @@ -0,0 +1,63 @@ +### Add functions here and link them to builder-bot format MUST BE "build-FunctionResourceName in template.yaml" + +build-QuoteServiceALB: builder-bot + +# Helper commands +build: + sam build + +deploy: + sam deploy + +logs: + sam logs --stack-name QuoteServiceALB + +tail: + sam logs --stack-name QuoteServiceALB --tail + +local: + swift run QuoteServiceALB + +local-invoke: + sam local invoke QuoteServiceALB --event events/GetQuote.json + +###################### No Change required below this line ########################## + +builder-bot: + $(eval $@PRODUCT = $(subst build-,,$(MAKECMDGOALS))) + $(eval $@BUILD_DIR = $(PWD)/.aws-sam/build-swift) + $(eval $@STAGE = $($@BUILD_DIR)/lambda) + $(eval $@ARTIFACTS_DIR = $(PWD)/.aws-sam/build/$($@PRODUCT)) + +## Building from swift-openapi-lambda in a local directory (not from Github) +## 2. Change `Package.swift` dependency to path: "../.." + +## 3. add /../.. to BUILD_SRC + $(eval $@BUILD_SRC = $(PWD)/../..) +## $(eval $@BUILD_SRC = $(PWD)) + +## 4. add `cd Examples/quoteapi-alb &&` to the docker BUILD_CMD + $(eval $@BUILD_CMD = "cd Examples/quoteapi-alb && swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target") +## $(eval $@BUILD_CMD = "swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target") + + # build docker image to compile Swift for Linux + docker build -f Dockerfile . -t swift-builder + + # prep directories + mkdir -p $($@BUILD_DIR)/lambda $($@ARTIFACTS_DIR) + + # compile application inside Docker image using source code from local project folder + + docker run --rm -v $($@BUILD_DIR):/build-target -v $($@BUILD_SRC):/build-src -w /build-src swift-builder bash -cl $($@BUILD_CMD) + + # create lambda bootstrap file + docker run --rm -v $($@BUILD_DIR):/build-target -v `pwd`:/build-src -w /build-src swift-builder bash -cl "cd /build-target/lambda && ln -s $($@PRODUCT) /bootstrap" + + # copy binary to stage + cp $($@BUILD_DIR)/release/$($@PRODUCT) $($@STAGE)/bootstrap + + # copy resources to stage (if they exist) + [ -d "$($@BUILD_DIR)/release/$($@PRODUCT)_$($@PRODUCT).resources" ] && cp $($@BUILD_DIR)/release/$($@PRODUCT)_$($@PRODUCT).resources/* $($@STAGE) || true + + # copy app from stage to artifacts dir + cp $($@STAGE)/* $($@ARTIFACTS_DIR) \ No newline at end of file diff --git a/Examples/quoteapi-alb/Package.swift b/Examples/quoteapi-alb/Package.swift new file mode 100644 index 0000000..c1924cf --- /dev/null +++ b/Examples/quoteapi-alb/Package.swift @@ -0,0 +1,43 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "QuoteServiceALB", + platforms: [ + .macOS(.v15) + ], + products: [ + .executable(name: "QuoteServiceALB", targets: ["QuoteServiceALB"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.4.0"), + .package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.8.2"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.2.0"), + .package(name: "swift-openapi-lambda", path: "../.."), + ], + targets: [ + .executableTarget( + name: "QuoteServiceALB", + dependencies: [ + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + .product(name: "OpenAPILambda", package: "swift-openapi-lambda"), + ], + path: "Sources/QuoteAPI", + resources: [ + .copy("openapi.yaml"), + .copy("openapi-generator-config.yaml"), + ], + plugins: [ + .plugin( + name: "OpenAPIGenerator", + package: "swift-openapi-generator" + ) + ] + ) + ] +) diff --git a/Examples/quoteapi-alb/README.md b/Examples/quoteapi-alb/README.md new file mode 100644 index 0000000..46b79b2 --- /dev/null +++ b/Examples/quoteapi-alb/README.md @@ -0,0 +1,104 @@ +# QuoteAPI ALB Example + +This application illustrates how to deploy a Server-Side Swift workload on AWS using an Application Load Balancer (ALB) with Lambda targets. The workload is a simple REST API that returns stock quotes. Requests to the ALB are forwarded to an AWS Lambda Function written in Swift using the OpenAPI Lambda library. + +## Prerequisites + +To build this sample application, you need: + +- [AWS Account](https://console.aws.amazon.com/) +- [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) - install the CLI and [configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) it with credentials to your AWS account +- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) - a command-line tool used to create serverless workloads on AWS +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) - to compile your Swift code for Linux deployment to AWS Lambda + +## Build the application + +The **sam build** command uses Docker to compile your Swift Lambda function and package it for deployment to AWS. + +```bash +sam build +``` + +On macOS, you might need to run this command if `sam` doesn't see `docker`: +```bash +export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock +``` + +## Deploy the application + +The **sam deploy** command creates the Lambda function, Application Load Balancer, and associated VPC resources in your AWS account. + +```bash +sam deploy --guided +``` + +## Use the API + +At the end of the deployment, SAM displays the endpoint of your Application Load Balancer: + +```bash +Outputs +---------------------------------------------------------------------------------------- +Key QuoteAPILoadBalancerURL +Description Application Load Balancer URL for QuoteAPI +Value http://QuoteAPILoadBalancer-123456789.us-east-1.elb.amazonaws.com/stocks/AAPL +---------------------------------------------------------------------------------------- +``` + +Use cURL or a tool such as [Postman](https://www.postman.com/) to interact with your API. Replace **[your-alb-endpoint]** with the QuoteAPILoadBalancerURL value from the deployment output. + +**Invoke the API Endpoint** + +```bash +curl http://[your-alb-endpoint]/stocks/AMZN +``` + +## Test the API Locally + +SAM also allows you to execute your Lambda functions locally on your development computer. + +**Invoke the Lambda Function Locally** + +```bash +sam local invoke QuoteServiceALB --event events/GetQuote.json +``` + +On macOS, you might need to run this command if `sam` doesn't see `docker`: +```bash +export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock +``` + +## Architecture + +This example demonstrates: + +- **Application Load Balancer**: Routes HTTP requests to Lambda functions +- **Lambda Target Group**: Configures the ALB to forward requests to Lambda +- **VPC Setup**: Creates a VPC with public subnets for the ALB +- **Security Groups**: Controls inbound traffic to the ALB +- **OpenAPI Integration**: Uses Swift OpenAPI Lambda library with ALB events + +## Cleanup + +When finished with your application, use SAM to delete it from your AWS account. Answer **Yes (y)** to all prompts. This will delete all of the application resources created in your AWS account. + +```bash +sam delete +``` + +> **⚠️ Security and Reliability Notice** +> +> This is an example application for demonstration purposes. When deploying such infrastructure in production environments, we strongly encourage you to follow these best practices for improved security and resiliency: +> +> - Enable access logging on Application Load Balancer ([documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html)) +> - Ensure that AWS Lambda function is configured for function-level concurrent execution limit ([concurrency documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html), [configuration guide](https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html)) +> - Check encryption settings for Lambda environment variables ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html)) +> - Ensure that AWS Lambda function is configured for a Dead Letter Queue (DLQ) ([documentation](https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq)) +> - Configure HTTPS/TLS termination on the Application Load Balancer ([documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-https-listener.html)) +> +> **Note:** The `openapi.yaml` file in this example is not suited for production. In real-world scenarios, you must: +> 1. Ensure that the global security field has rules defined +> 2. Ensure that security operations is not empty ([OpenAPI Security Specification](https://learn.openapis.org/specification/security.html)) +> 3. Follow proper authentication, authorization, input validation, and error handling practices +> +> As per Checkov CKV_OPENAPI_4 and CKV_OPENAPI_5 security checks. diff --git a/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift new file mode 100644 index 0000000..6e03fd7 --- /dev/null +++ b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates +// and the Swift OpenAPI Lambda project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import Logging +import OpenAPIRuntime +import OpenAPILambda + +@main +struct QuoteServiceALBImpl: APIProtocol, OpenAPILambdaALB { + + let logger: Logger + + func register(transport: OpenAPILambdaTransport) throws { + + // OPTIONAL + // you have a chance here to customize the routes, for example + try transport.router.get("/health") { _, _ in + "OK" + } + logger.trace("Available Routes\n\(transport.router)") // print the router tree (for debugging purposes) + + // OPTIONAL + // to log all requests and their responses, add a logging middleware + let loggingMiddleware = LoggingMiddleware(logger: logger) + + // MANDATORY (middlewares are optional) + try self.registerHandlers(on: transport, middlewares: [loggingMiddleware]) + } + + static func main() async throws { + let openAPIService = QuoteServiceALBImpl() + try await openAPIService.run() + } + + init() { + var logger = Logger(label: "QuoteServiceALB") + logger.logLevel = .trace + self.logger = logger + } + + func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output { + logger.trace("GetQuote - Started") + + let symbol = input.path.symbol + + var date: Date = Date() + if let dateString = input.query.date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyyMMdd" + date = dateFormatter.date(from: dateString) ?? Date() + } + + let price = Components.Schemas.quote( + symbol: symbol, + price: Double.random(in: 100..<150).rounded(), + change: Double.random(in: -5..<5).rounded(), + changePercent: Double.random(in: -0.05..<0.05), + volume: Double.random(in: 10000..<100000).rounded(), + timestamp: date + ) + + logger.trace("GetQuote - Returning") + + return .ok(.init(body: .json(price))) + } +} diff --git a/Examples/quoteapi-alb/Sources/QuoteAPI/openapi-generator-config.yaml b/Examples/quoteapi-alb/Sources/QuoteAPI/openapi-generator-config.yaml new file mode 100644 index 0000000..99604c2 --- /dev/null +++ b/Examples/quoteapi-alb/Sources/QuoteAPI/openapi-generator-config.yaml @@ -0,0 +1,3 @@ +generate: + - types + - server \ No newline at end of file diff --git a/Examples/quoteapi-alb/Sources/QuoteAPI/openapi.yaml b/Examples/quoteapi-alb/Sources/QuoteAPI/openapi.yaml new file mode 100644 index 0000000..d8e4147 --- /dev/null +++ b/Examples/quoteapi-alb/Sources/QuoteAPI/openapi.yaml @@ -0,0 +1,54 @@ +openapi: 3.1.0 +info: + title: StockQuoteService + version: 1.0.0 + +components: + schemas: + quote: + type: object + properties: + symbol: + type: string + price: + type: number + change: + type: number + changePercent: + type: number + volume: + type: number + timestamp: + type: string + format: date-time + +paths: + /stocks/{symbol}: + get: + summary: Get the latest quote for a stock + operationId: getQuote + parameters: + - name: symbol + in: path + required: true + schema: + type: string + - name: date + in: query + required: false + schema: + type: string + format: date + tags: + - stocks + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/quote' + 400: + description: Bad Request + 404: + description: Not Found \ No newline at end of file diff --git a/Examples/quoteapi-alb/events/GetQuote.json b/Examples/quoteapi-alb/events/GetQuote.json new file mode 100644 index 0000000..9187b4e --- /dev/null +++ b/Examples/quoteapi-alb/events/GetQuote.json @@ -0,0 +1,25 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/50dc6c495c0c9188" + } + }, + "httpMethod": "GET", + "path": "/stocks/AAPL", + "queryStringParameters": {}, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip, deflate", + "accept-language": "en-US,en;q=0.9", + "connection": "keep-alive", + "host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36", + "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476", + "x-forwarded-for": "72.12.164.125", + "x-forwarded-port": "80", + "x-forwarded-proto": "http" + }, + "body": "", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/Examples/quoteapi-alb/template.yaml b/Examples/quoteapi-alb/template.yaml new file mode 100644 index 0000000..ad663d8 --- /dev/null +++ b/Examples/quoteapi-alb/template.yaml @@ -0,0 +1,149 @@ +# This is an example SAM template for the purpose of this project. +# When deploying such infrastructure in production environment, +# we strongly encourage you to follow these best practices for improved security and resiliency +# - Enable access loggin on API Gateway +# See: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html) +# - Ensure that AWS Lambda function is configured for function-level concurrent execution limit +# See: https://docs.aws.amazon.com/lambda/latest/dg/lambda-concurrency.html +# https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html +# - Check encryption settings for Lambda environment variable +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars-encryption.html +# - Ensure that AWS Lambda function is configured for a Dead Letter Queue(DLQ) +# See: https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-dlq +# - Ensure that AWS Lambda function is configured inside a VPC when it needs to access private resources +# See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html +# Code Example: https://github.com/awslabs/swift-aws-lambda-runtime/tree/main/Examples/ServiceLifecycle%2BPostgres + +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: QuoteAPI ALB Example + +Resources: + QuoteServiceALB: + Type: AWS::Serverless::Function + Properties: + CodeUri: . + Handler: bootstrap + Runtime: provided.al2023 + Architectures: + - arm64 + MemorySize: 128 + Timeout: 30 + Environment: + Variables: + LOG_LEVEL: trace + Metadata: + BuildMethod: makefile + + # Lambda permission for ALB + ALBLambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt QuoteServiceALB.Arn + Action: lambda:InvokeFunction + Principal: elasticloadbalancing.amazonaws.com + + # Target Group for Lambda + ALBTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + DependsOn: ALBLambdaInvokePermission + Properties: + TargetType: lambda + Targets: + - Id: !GetAtt QuoteServiceALB.Arn + + # Application Load Balancer + ApplicationLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + Subnets: + - !Ref PublicSubnet1 + - !Ref PublicSubnet2 + SecurityGroups: + - !Ref ALBSecurityGroup + + # Listener + ALBListener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + LoadBalancerArn: !Ref ApplicationLoadBalancer + Port: 80 + Protocol: HTTP + DefaultActions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroup + + # VPC for ALB + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + + PublicSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: 10.0.1.0/24 + AvailabilityZone: !Select [0, !GetAZs ''] + MapPublicIpOnLaunch: true + + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + CidrBlock: 10.0.2.0/24 + AvailabilityZone: !Select [1, !GetAZs ''] + MapPublicIpOnLaunch: true + + InternetGateway: + Type: AWS::EC2::InternetGateway + + AttachGateway: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + VpcId: !Ref VPC + InternetGatewayId: !Ref InternetGateway + + RouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + + Route: + Type: AWS::EC2::Route + DependsOn: AttachGateway + Properties: + RouteTableId: !Ref RouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + SubnetRouteTableAssociation1: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet1 + RouteTableId: !Ref RouteTable + + SubnetRouteTableAssociation2: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + SubnetId: !Ref PublicSubnet2 + RouteTableId: !Ref RouteTable + + ALBSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Security group for ALB + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 80 + ToPort: 80 + CidrIp: 0.0.0.0/0 + +Outputs: + ALBUrl: + Description: Application Load Balancer URL + Value: !Sub "http://${ApplicationLoadBalancer.DNSName}/stocks/AAPL" \ No newline at end of file diff --git a/Examples/quoteapi/.gitignore b/Examples/quoteapi-apigtw/.gitignore similarity index 100% rename from Examples/quoteapi/.gitignore rename to Examples/quoteapi-apigtw/.gitignore diff --git a/Examples/quoteapi-apigtw/Dockerfile b/Examples/quoteapi-apigtw/Dockerfile new file mode 100644 index 0000000..fbcbc27 --- /dev/null +++ b/Examples/quoteapi-apigtw/Dockerfile @@ -0,0 +1,3 @@ +# image used to compile your Swift code +FROM public.ecr.aws/docker/library/swift:6.2-amazonlinux2 +RUN yum -y install git jq tar zip openssl-devel diff --git a/Examples/quoteapi/Makefile b/Examples/quoteapi-apigtw/Makefile similarity index 91% rename from Examples/quoteapi/Makefile rename to Examples/quoteapi-apigtw/Makefile index a316e90..3745236 100644 --- a/Examples/quoteapi/Makefile +++ b/Examples/quoteapi-apigtw/Makefile @@ -41,8 +41,8 @@ builder-bot: $(eval $@BUILD_SRC = $(PWD)/../..) ## $(eval $@BUILD_SRC = $(PWD)) -## 4. add `cd Examples/quoteapi &&` to the docker BUILD_CMD - $(eval $@BUILD_CMD = "ls && cd Examples/quoteapi && swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target") +## 4. add `cd Examples/quoteapi-apigtw &&` to the docker BUILD_CMD + $(eval $@BUILD_CMD = "cd Examples/quoteapi-apigtw && swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target") ## $(eval $@BUILD_CMD = "swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target") # build docker image to compile Swift for Linux diff --git a/Examples/quoteapi/Package.swift b/Examples/quoteapi-apigtw/Package.swift similarity index 100% rename from Examples/quoteapi/Package.swift rename to Examples/quoteapi-apigtw/Package.swift diff --git a/Examples/quoteapi/README.md b/Examples/quoteapi-apigtw/README.md similarity index 100% rename from Examples/quoteapi/README.md rename to Examples/quoteapi-apigtw/README.md diff --git a/Examples/quoteapi/Sources/LambdaAuthorizer/main.swift b/Examples/quoteapi-apigtw/Sources/LambdaAuthorizer/main.swift similarity index 100% rename from Examples/quoteapi/Sources/LambdaAuthorizer/main.swift rename to Examples/quoteapi-apigtw/Sources/LambdaAuthorizer/main.swift diff --git a/Examples/quoteapi/Sources/QuoteAPI/AuthenticateUserMiddleware.swift b/Examples/quoteapi-apigtw/Sources/QuoteAPI/AuthenticateUserMiddleware.swift similarity index 100% rename from Examples/quoteapi/Sources/QuoteAPI/AuthenticateUserMiddleware.swift rename to Examples/quoteapi-apigtw/Sources/QuoteAPI/AuthenticateUserMiddleware.swift diff --git a/Examples/quoteapi/Sources/QuoteAPI/QuoteService.swift b/Examples/quoteapi-apigtw/Sources/QuoteAPI/QuoteService.swift similarity index 100% rename from Examples/quoteapi/Sources/QuoteAPI/QuoteService.swift rename to Examples/quoteapi-apigtw/Sources/QuoteAPI/QuoteService.swift diff --git a/Examples/quoteapi/Sources/QuoteAPI/openapi-generator-config.yaml b/Examples/quoteapi-apigtw/Sources/QuoteAPI/openapi-generator-config.yaml similarity index 100% rename from Examples/quoteapi/Sources/QuoteAPI/openapi-generator-config.yaml rename to Examples/quoteapi-apigtw/Sources/QuoteAPI/openapi-generator-config.yaml diff --git a/Examples/quoteapi/Sources/QuoteAPI/openapi.yaml b/Examples/quoteapi-apigtw/Sources/QuoteAPI/openapi.yaml similarity index 100% rename from Examples/quoteapi/Sources/QuoteAPI/openapi.yaml rename to Examples/quoteapi-apigtw/Sources/QuoteAPI/openapi.yaml diff --git a/Examples/quoteapi/events/404.json b/Examples/quoteapi-apigtw/events/404.json similarity index 100% rename from Examples/quoteapi/events/404.json rename to Examples/quoteapi-apigtw/events/404.json diff --git a/Examples/quoteapi/events/GetQuote.json b/Examples/quoteapi-apigtw/events/GetQuote.json similarity index 100% rename from Examples/quoteapi/events/GetQuote.json rename to Examples/quoteapi-apigtw/events/GetQuote.json diff --git a/Examples/quoteapi/events/HealthCheck.json b/Examples/quoteapi-apigtw/events/HealthCheck.json similarity index 100% rename from Examples/quoteapi/events/HealthCheck.json rename to Examples/quoteapi-apigtw/events/HealthCheck.json diff --git a/Examples/quoteapi/template.yml b/Examples/quoteapi-apigtw/template.yml similarity index 100% rename from Examples/quoteapi/template.yml rename to Examples/quoteapi-apigtw/template.yml diff --git a/Sources/ALB/ALBTargetGroup+HTTPRequest.swift b/Sources/ALB/ALBTargetGroup+HTTPRequest.swift new file mode 100644 index 0000000..62e0ab5 --- /dev/null +++ b/Sources/ALB/ALBTargetGroup+HTTPRequest.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import AWSLambdaEvents +import HTTPTypes +import OpenAPIRuntime + +extension ALBTargetGroupRequest { + + /// Return an `HTTPRequest` for this `ALBTargetGroupRequest` + public func httpRequest() throws -> HTTPRequest { + HTTPRequest( + method: self.httpMethod, + scheme: nil, + authority: nil, + path: self.path, + headerFields: self.headers?.httpFields() ?? [:] + ) + } +} + +extension ALBTargetGroupResponse { + + /// Create a `APIGatewayV2Response` from an `HTTPResponse` + public init(from response: HTTPResponse) { + self = ALBTargetGroupResponse( + statusCode: response.status, + statusDescription: response.debugDescription, + headers: .init(from: response.headerFields), + multiValueHeaders: nil, + isBase64Encoded: false + ) + } +} diff --git a/Sources/ALB/OpenAPILambdaALB.swift b/Sources/ALB/OpenAPILambdaALB.swift new file mode 100644 index 0000000..b7f19ae --- /dev/null +++ b/Sources/ALB/OpenAPILambdaALB.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation +import AWSLambdaRuntime +import AWSLambdaEvents +import OpenAPIRuntime +import HTTPTypes + +/// An specialization of the `OpenAPILambda` protocol that works with Amazon API Gateway HTTP Mode, aka API Gateway v2 +public protocol OpenAPILambdaALB: OpenAPILambdaService +where + Event == ALBTargetGroupRequest, + Output == ALBTargetGroupResponse +{} + + +extension OpenAPILambdaALB { + /// Transform a Lambda input (`APIGatewayV2Request` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`) + public func request(context: LambdaContext, from request: Event) throws -> OpenAPILambdaRequest { + (try request.httpRequest(), request.body) + } + + /// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`APIGatewayV2Response`) + public func output(from response: OpenAPILambdaResponse) -> Output { + var apiResponse = ALBTargetGroupResponse(from: response.0) + apiResponse.body = response.1 ?? "" + return apiResponse + } +} From 81d3dc15bb853b8b1e8023603790f3fe02d8af43 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 11:45:13 +0200 Subject: [PATCH 2/7] add unit tests --- Package.swift | 3 +- .../ALBConversionTests.swift | 73 ++++++++++ .../APIGatewayV2ConversionTests.swift | 127 ++++++++++++++++++ .../HTTPHeadersConversionTests.swift | 48 +++++++ 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 Tests/OpenAPILambdaTests/ALBConversionTests.swift create mode 100644 Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift create mode 100644 Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift diff --git a/Package.swift b/Package.swift index aaa5259..8a66931 100644 --- a/Package.swift +++ b/Package.swift @@ -26,7 +26,8 @@ let package = Package( .testTarget( name: "OpenAPILambdaTests", dependencies: [ - .byName(name: "OpenAPILambda") + .byName(name: "OpenAPILambda"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ] ), ] diff --git a/Tests/OpenAPILambdaTests/ALBConversionTests.swift b/Tests/OpenAPILambdaTests/ALBConversionTests.swift new file mode 100644 index 0000000..68a4cfe --- /dev/null +++ b/Tests/OpenAPILambdaTests/ALBConversionTests.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import AWSLambdaEvents +import Foundation +import HTTPTypes +import Testing + +@testable import OpenAPILambda + +struct ALBConversionTests { + + static let albEventJSON = """ + { + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/50dc6c495c0c9188" + } + }, + "httpMethod": "GET", + "path": "/stocks/AAPL", + "queryStringParameters": {}, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + "body": "", + "isBase64Encoded": false + } + """ + + @Test("ALB request to HTTPRequest conversion") + func testALBRequestToHTTPRequest() throws { + let data = ALBConversionTests.albEventJSON.data(using: .utf8)! + let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) + + let httpRequest = try albRequest.httpRequest() + + #expect(httpRequest.method == HTTPRequest.Method.get) + #expect(httpRequest.path == "/stocks/AAPL") + #expect(httpRequest.headerFields[HTTPField.Name.accept] == "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + #expect(httpRequest.headerFields[HTTPField.Name("host")!] == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") + #expect(httpRequest.headerFields[HTTPField.Name.userAgent] == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + } + + @Test("HTTPResponse to ALB response conversion") + func testHTTPResponseToALBResponse() throws { + var httpResponse = HTTPResponse(status: .ok) + httpResponse.headerFields[HTTPField.Name.contentType] = "application/json" + httpResponse.headerFields[HTTPField.Name.contentLength] = "42" + + let albResponse = ALBTargetGroupResponse(from: httpResponse) + + #expect(albResponse.statusCode == .ok) + #expect(albResponse.headers?[HTTPField.Name.contentType.rawName] == "application/json") + #expect(albResponse.headers?[HTTPField.Name.contentLength.rawName] == "42") + #expect(albResponse.isBase64Encoded == false) + } + + +} \ No newline at end of file diff --git a/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift new file mode 100644 index 0000000..67a79b2 --- /dev/null +++ b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift @@ -0,0 +1,127 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import AWSLambdaEvents +import Foundation +import HTTPTypes +import Testing + +@testable import OpenAPILambda + +struct APIGatewayV2ConversionTests { + + static let apiGatewayEventJSON = """ + { + "rawQueryString": "", + "headers": { + "host": "b2k1t8fon7.execute-api.us-east-1.amazonaws.com", + "accept": "*/*", + "user-agent": "curl/8.1.2", + "authorization": "Bearer 123" + }, + "requestContext": { + "apiId": "b2k1t8fon7", + "http": { + "sourceIp": "191.95.148.219", + "userAgent": "curl/8.1.2", + "method": "GET", + "path": "/stocks/AAPL", + "protocol": "HTTP/1.1" + }, + "timeEpoch": 1701957940365, + "domainPrefix": "b2k1t8fon7", + "accountId": "486652066693", + "time": "07/Dec/2023:14:05:40 +0000", + "stage": "$default", + "domainName": "b2k1t8fon7.execute-api.us-east-1.amazonaws.com", + "requestId": "Pk2gOia2IAMEPOw=" + }, + "isBase64Encoded": false, + "version": "2.0", + "routeKey": "$default", + "rawPath": "/stocks/AAPL" + } + """ + + static let apiGatewayEventWithQueryJSON = """ + { + "rawQueryString": "limit=10&offset=0", + "headers": { + "host": "b2k1t8fon7.execute-api.us-east-1.amazonaws.com" + }, + "requestContext": { + "apiId": "b2k1t8fon7", + "http": { + "sourceIp": "191.95.148.219", + "userAgent": "curl/8.1.2", + "method": "GET", + "path": "/stocks/AAPL", + "protocol": "HTTP/1.1" + }, + "timeEpoch": 1701957940365, + "domainPrefix": "b2k1t8fon7", + "accountId": "486652066693", + "time": "07/Dec/2023:14:05:40 +0000", + "stage": "$default", + "domainName": "b2k1t8fon7.execute-api.us-east-1.amazonaws.com", + "requestId": "Pk2gOia2IAMEPOw=" + }, + "isBase64Encoded": false, + "version": "2.0", + "routeKey": "$default", + "rawPath": "/stocks/AAPL" + } + """ + + @Test("API Gateway v2 request to HTTPRequest conversion") + func testAPIGatewayV2RequestToHTTPRequest() throws { + let data = APIGatewayV2ConversionTests.apiGatewayEventJSON.data(using: .utf8)! + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) + + let httpRequest = try apiGatewayRequest.httpRequest() + + #expect(httpRequest.method == HTTPRequest.Method.get) + #expect(httpRequest.path == "/stocks/AAPL") + #expect(httpRequest.scheme == "https") + #expect(httpRequest.headerFields[HTTPField.Name("host")!] == "b2k1t8fon7.execute-api.us-east-1.amazonaws.com") + #expect(httpRequest.headerFields[HTTPField.Name.accept] == "*/*") + #expect(httpRequest.headerFields[HTTPField.Name.userAgent] == "curl/8.1.2") + #expect(httpRequest.headerFields[HTTPField.Name.authorization] == "Bearer 123") + } + + @Test("API Gateway v2 request with query string") + func testAPIGatewayV2RequestWithQueryString() throws { + let data = APIGatewayV2ConversionTests.apiGatewayEventWithQueryJSON.data(using: .utf8)! + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) + + let httpRequest = try apiGatewayRequest.httpRequest() + + #expect(httpRequest.path == "/stocks/AAPL?limit=10&offset=0") + } + + @Test("HTTPResponse to API Gateway v2 response conversion") + func testHTTPResponseToAPIGatewayV2Response() throws { + var httpResponse = HTTPResponse(status: .ok) + httpResponse.headerFields[HTTPField.Name.contentType] = "application/json" + httpResponse.headerFields[HTTPField.Name.contentLength] = "42" + + let apiGatewayResponse = APIGatewayV2Response(from: httpResponse) + + #expect(apiGatewayResponse.statusCode == .ok) + #expect(apiGatewayResponse.headers?[HTTPField.Name.contentType.rawName] == "application/json") + #expect(apiGatewayResponse.headers?[HTTPField.Name.contentLength.rawName] == "42") + #expect(apiGatewayResponse.isBase64Encoded == false) + #expect(apiGatewayResponse.cookies == nil) + } +} \ No newline at end of file diff --git a/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift new file mode 100644 index 0000000..08a5dfd --- /dev/null +++ b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import AWSLambdaEvents +import HTTPTypes +import Testing + +@testable import OpenAPILambda + +@Suite("HTTP Headers Conversion Tests") +struct HTTPHeadersConversionTests { + + @Test("Multi-value headers preserved as comma-separated") + func testMultiValueHeadersPreserved() throws { + var httpResponse = HTTPResponse(status: .ok) + httpResponse.headerFields.append(HTTPField(name: HTTPField.Name.setCookie, value: "session=abc123")) + httpResponse.headerFields.append(HTTPField(name: HTTPField.Name.setCookie, value: "theme=dark")) + + let albResponse = ALBTargetGroupResponse(from: httpResponse) + + #expect(albResponse.headers?[HTTPField.Name.setCookie.rawName] == "session=abc123, theme=dark") + #expect(albResponse.multiValueHeaders == nil) + } + + @Test("HTTPHeaders to HTTPFields conversion") + func testHTTPHeadersToHTTPFields() throws { + let headers: HTTPHeaders = [ + "Set-Cookie": "session=abc123, theme=dark", + "Content-Type": "application/json" + ] + + let httpFields = headers.httpFields() + + #expect(httpFields[HTTPField.Name.setCookie] == "session=abc123, theme=dark") + #expect(httpFields[HTTPField.Name.contentType] == "application/json") + } +} \ No newline at end of file From b4b5ae30695135baf1766cbb7cd87cfc4bd1ac37 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 11:45:44 +0200 Subject: [PATCH 3/7] address code review feedback --- Examples/quoteapi-alb/template.yaml | 2 +- Makefile | 2 + .../ALB/ALBTargetGroup+HTTPRequest.swift | 6 +-- .../ALB/OpenAPILambdaALB.swift | 6 +-- .../EventSource/HTTPHeadersExtension.swift | 38 +++++++++++++++++++ .../HttpApi/APIGatewayV2+HTTPRequest.swift | 18 +-------- .../HttpApi/OpenAPILambdaHttpApi.swift | 0 7 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 Makefile rename Sources/{ => EventSource}/ALB/ALBTargetGroup+HTTPRequest.swift (88%) rename Sources/{ => EventSource}/ALB/OpenAPILambdaALB.swift (85%) create mode 100644 Sources/EventSource/HTTPHeadersExtension.swift rename Sources/{ => EventSource}/HttpApi/APIGatewayV2+HTTPRequest.swift (73%) rename Sources/{ => EventSource}/HttpApi/OpenAPILambdaHttpApi.swift (100%) diff --git a/Examples/quoteapi-alb/template.yaml b/Examples/quoteapi-alb/template.yaml index ad663d8..e9f98c1 100644 --- a/Examples/quoteapi-alb/template.yaml +++ b/Examples/quoteapi-alb/template.yaml @@ -63,7 +63,7 @@ Resources: SecurityGroups: - !Ref ALBSecurityGroup - # Listener + # HTTP Listener (HTTPS requires valid domain certificate) ALBListener: Type: AWS::ElasticLoadBalancingV2::Listener Properties: diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b4acd87 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +format: + @swift format -i -r Package.swift Examples Sources \ No newline at end of file diff --git a/Sources/ALB/ALBTargetGroup+HTTPRequest.swift b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift similarity index 88% rename from Sources/ALB/ALBTargetGroup+HTTPRequest.swift rename to Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift index 62e0ab5..b0c332b 100644 --- a/Sources/ALB/ALBTargetGroup+HTTPRequest.swift +++ b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift @@ -22,8 +22,8 @@ extension ALBTargetGroupRequest { public func httpRequest() throws -> HTTPRequest { HTTPRequest( method: self.httpMethod, - scheme: nil, - authority: nil, + scheme: self.headers?["X-Forwarded-Proto"], + authority: self.headers?["Host"], path: self.path, headerFields: self.headers?.httpFields() ?? [:] ) @@ -32,7 +32,7 @@ extension ALBTargetGroupRequest { extension ALBTargetGroupResponse { - /// Create a `APIGatewayV2Response` from an `HTTPResponse` + /// Create an `ALBTargetGroupResponse` from an `HTTPResponse` public init(from response: HTTPResponse) { self = ALBTargetGroupResponse( statusCode: response.status, diff --git a/Sources/ALB/OpenAPILambdaALB.swift b/Sources/EventSource/ALB/OpenAPILambdaALB.swift similarity index 85% rename from Sources/ALB/OpenAPILambdaALB.swift rename to Sources/EventSource/ALB/OpenAPILambdaALB.swift index b7f19ae..5e26373 100644 --- a/Sources/ALB/OpenAPILambdaALB.swift +++ b/Sources/EventSource/ALB/OpenAPILambdaALB.swift @@ -18,7 +18,7 @@ import AWSLambdaEvents import OpenAPIRuntime import HTTPTypes -/// An specialization of the `OpenAPILambda` protocol that works with Amazon API Gateway HTTP Mode, aka API Gateway v2 +/// An specialization of the `OpenAPILambda` protocol that works with an Application Load Balancer public protocol OpenAPILambdaALB: OpenAPILambdaService where Event == ALBTargetGroupRequest, @@ -27,12 +27,12 @@ where extension OpenAPILambdaALB { - /// Transform a Lambda input (`APIGatewayV2Request` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`) + /// Transform a Lambda input (`ALBTargetGroupRequest` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`) public func request(context: LambdaContext, from request: Event) throws -> OpenAPILambdaRequest { (try request.httpRequest(), request.body) } - /// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`APIGatewayV2Response`) + /// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`ALBTargetGroupResponse`) public func output(from response: OpenAPILambdaResponse) -> Output { var apiResponse = ALBTargetGroupResponse(from: response.0) apiResponse.body = response.1 ?? "" diff --git a/Sources/EventSource/HTTPHeadersExtension.swift b/Sources/EventSource/HTTPHeadersExtension.swift new file mode 100644 index 0000000..3303533 --- /dev/null +++ b/Sources/EventSource/HTTPHeadersExtension.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenAPI Lambda open source project +// +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift OpenAPI Lambda project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import AWSLambdaEvents +import HTTPTypes + +public extension HTTPHeaders { + /// Create an `HTTPFields` (from `HTTPTypes` library) from this APIGateway `HTTPHeader` + func httpFields() -> HTTPFields { + HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) }) + } + + /// Create HTTPHeaders from HTTPFields + init(from fields: HTTPFields) { + var headers: HTTPHeaders = [:] + for field in fields { + let name = field.name.rawName + if let existing = headers[name] { + headers[name] = "\(existing), \(field.value)" + } + else { + headers[name] = field.value + } + } + self = headers + } +} diff --git a/Sources/HttpApi/APIGatewayV2+HTTPRequest.swift b/Sources/EventSource/HttpApi/APIGatewayV2+HTTPRequest.swift similarity index 73% rename from Sources/HttpApi/APIGatewayV2+HTTPRequest.swift rename to Sources/EventSource/HttpApi/APIGatewayV2+HTTPRequest.swift index 3231e0e..2cd35fe 100644 --- a/Sources/HttpApi/APIGatewayV2+HTTPRequest.swift +++ b/Sources/EventSource/HttpApi/APIGatewayV2+HTTPRequest.swift @@ -27,8 +27,8 @@ extension APIGatewayV2Request { public func httpRequest() throws -> HTTPRequest { HTTPRequest( method: self.context.http.method, - scheme: "https", - authority: "", + scheme: "https", // APIGateway is always HTTPS + authority: self.headers["Host"], path: pathWithQueryString, headerFields: self.headers.httpFields() ) @@ -47,17 +47,3 @@ extension APIGatewayV2Response { ) } } - -public extension HTTPHeaders { - /// Create an `HTTPFields` (from `HTTPTypes` library) from this APIGateway `HTTPHeader` - func httpFields() -> HTTPFields { - HTTPFields(self.map { key, value in HTTPField(name: .init(key)!, value: value) }) - } - - /// Create HTTPHeaders from HTTPFields - init(from fields: HTTPFields) { - var headers: HTTPHeaders = [:] - fields.forEach { headers[$0.name.rawName] = $0.value } - self = headers - } -} diff --git a/Sources/HttpApi/OpenAPILambdaHttpApi.swift b/Sources/EventSource/HttpApi/OpenAPILambdaHttpApi.swift similarity index 100% rename from Sources/HttpApi/OpenAPILambdaHttpApi.swift rename to Sources/EventSource/HttpApi/OpenAPILambdaHttpApi.swift From b6b6a9a589d0418bd7d3dba396fcb2d06c65f62e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 11:51:27 +0200 Subject: [PATCH 4/7] more unit tests --- .../ALBConversionTests.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Tests/OpenAPILambdaTests/ALBConversionTests.swift b/Tests/OpenAPILambdaTests/ALBConversionTests.swift index 68a4cfe..dcdefae 100644 --- a/Tests/OpenAPILambdaTests/ALBConversionTests.swift +++ b/Tests/OpenAPILambdaTests/ALBConversionTests.swift @@ -55,6 +55,66 @@ struct ALBConversionTests { #expect(httpRequest.headerFields[HTTPField.Name.userAgent] == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") } + @Test("ALB X-Forwarded-Proto and Host mapping") + func testALBForwardedHeaders() throws { + let albEventWithForwardedHeaders = """ + { + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/50dc6c495c0c9188" + } + }, + "httpMethod": "GET", + "path": "/stocks/AAPL", + "queryStringParameters": {}, + "headers": { + "Host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "X-Forwarded-Proto": "https" + }, + "body": "", + "isBase64Encoded": false + } + """ + + let data = albEventWithForwardedHeaders.data(using: .utf8)! + let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) + + let httpRequest = try albRequest.httpRequest() + + #expect(httpRequest.scheme == "https") + #expect(httpRequest.authority == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") + } + + @Test("ALB lowercase headers mapping") + func testALBLowercaseHeaders() throws { + let albEventWithLowercaseHeaders = """ + { + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/50dc6c495c0c9188" + } + }, + "httpMethod": "GET", + "path": "/stocks/AAPL", + "queryStringParameters": {}, + "headers": { + "host": "lambda-alb-123578498.us-east-1.elb.amazonaws.com", + "x-forwarded-proto": "https" + }, + "body": "", + "isBase64Encoded": false + } + """ + + let data = albEventWithLowercaseHeaders.data(using: .utf8)! + let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) + + let httpRequest = try albRequest.httpRequest() + + #expect(httpRequest.scheme == nil) + #expect(httpRequest.authority == nil) + } + @Test("HTTPResponse to ALB response conversion") func testHTTPResponseToALBResponse() throws { var httpResponse = HTTPResponse(status: .ok) From b3844f7d8fcc05aa538fdbdd3e1a51c28fad8e9e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 11:57:33 +0200 Subject: [PATCH 5/7] allow for mixed case header names --- .../EventSource/ALB/ALBTargetGroup+HTTPRequest.swift | 12 ++++++++---- Tests/OpenAPILambdaTests/ALBConversionTests.swift | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift index b0c332b..f5dedf6 100644 --- a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift +++ b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift @@ -20,12 +20,16 @@ extension ALBTargetGroupRequest { /// Return an `HTTPRequest` for this `ALBTargetGroupRequest` public func httpRequest() throws -> HTTPRequest { - HTTPRequest( + let headers = self.headers ?? [:] + let scheme = headers.first { $0.key.lowercased() == "x-forwarded-proto" }?.value + let authority = headers.first { $0.key.lowercased() == "host" }?.value + + return HTTPRequest( method: self.httpMethod, - scheme: self.headers?["X-Forwarded-Proto"], - authority: self.headers?["Host"], + scheme: scheme, + authority: authority, path: self.path, - headerFields: self.headers?.httpFields() ?? [:] + headerFields: headers.httpFields() ) } } diff --git a/Tests/OpenAPILambdaTests/ALBConversionTests.swift b/Tests/OpenAPILambdaTests/ALBConversionTests.swift index dcdefae..6340abc 100644 --- a/Tests/OpenAPILambdaTests/ALBConversionTests.swift +++ b/Tests/OpenAPILambdaTests/ALBConversionTests.swift @@ -111,8 +111,8 @@ struct ALBConversionTests { let httpRequest = try albRequest.httpRequest() - #expect(httpRequest.scheme == nil) - #expect(httpRequest.authority == nil) + #expect(httpRequest.scheme == "https") + #expect(httpRequest.authority == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") } @Test("HTTPResponse to ALB response conversion") From 8696f55375f970b14b1676e4b3b3ac5ea8a46154 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 12:09:35 +0200 Subject: [PATCH 6/7] fix ci --- .licenseignore | 1 + .../Sources/QuoteAPI/QuoteService.swift | 4 +- Makefile | 2 +- .../ALB/ALBTargetGroup+HTTPRequest.swift | 2 +- .../ALBConversionTests.swift | 44 +++++++++++-------- .../APIGatewayV2ConversionTests.swift | 24 +++++----- .../HTTPHeadersConversionTests.swift | 16 +++---- 7 files changed, 50 insertions(+), 43 deletions(-) diff --git a/.licenseignore b/.licenseignore index 57732c4..76696ce 100644 --- a/.licenseignore +++ b/.licenseignore @@ -33,3 +33,4 @@ Package.resolved *.yml *.json *.gif +Makefile \ No newline at end of file diff --git a/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift index 6e03fd7..246aa99 100644 --- a/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift +++ b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift @@ -2,8 +2,8 @@ // // This source file is part of the Swift OpenAPI Lambda open source project // -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates -// and the Swift OpenAPI Lambda project authors +// Copyright Swift OpenAPI Lambda project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Makefile b/Makefile index b4acd87..8e72bb6 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ format: - @swift format -i -r Package.swift Examples Sources \ No newline at end of file + @swift format -i -r Package.swift Examples Sources Tests \ No newline at end of file diff --git a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift index f5dedf6..1e70483 100644 --- a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift +++ b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift @@ -23,7 +23,7 @@ extension ALBTargetGroupRequest { let headers = self.headers ?? [:] let scheme = headers.first { $0.key.lowercased() == "x-forwarded-proto" }?.value let authority = headers.first { $0.key.lowercased() == "host" }?.value - + return HTTPRequest( method: self.httpMethod, scheme: scheme, diff --git a/Tests/OpenAPILambdaTests/ALBConversionTests.swift b/Tests/OpenAPILambdaTests/ALBConversionTests.swift index 6340abc..169baad 100644 --- a/Tests/OpenAPILambdaTests/ALBConversionTests.swift +++ b/Tests/OpenAPILambdaTests/ALBConversionTests.swift @@ -20,7 +20,7 @@ import Testing @testable import OpenAPILambda struct ALBConversionTests { - + static let albEventJSON = """ { "requestContext": { @@ -40,21 +40,27 @@ struct ALBConversionTests { "isBase64Encoded": false } """ - + @Test("ALB request to HTTPRequest conversion") func testALBRequestToHTTPRequest() throws { let data = ALBConversionTests.albEventJSON.data(using: .utf8)! let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) - + let httpRequest = try albRequest.httpRequest() - + #expect(httpRequest.method == HTTPRequest.Method.get) #expect(httpRequest.path == "/stocks/AAPL") - #expect(httpRequest.headerFields[HTTPField.Name.accept] == "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8") + #expect( + httpRequest.headerFields[HTTPField.Name.accept] + == "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + ) #expect(httpRequest.headerFields[HTTPField.Name("host")!] == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") - #expect(httpRequest.headerFields[HTTPField.Name.userAgent] == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + #expect( + httpRequest.headerFields[HTTPField.Name.userAgent] + == "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + ) } - + @Test("ALB X-Forwarded-Proto and Host mapping") func testALBForwardedHeaders() throws { let albEventWithForwardedHeaders = """ @@ -75,16 +81,16 @@ struct ALBConversionTests { "isBase64Encoded": false } """ - + let data = albEventWithForwardedHeaders.data(using: .utf8)! let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) - + let httpRequest = try albRequest.httpRequest() - + #expect(httpRequest.scheme == "https") #expect(httpRequest.authority == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") } - + @Test("ALB lowercase headers mapping") func testALBLowercaseHeaders() throws { let albEventWithLowercaseHeaders = """ @@ -105,29 +111,29 @@ struct ALBConversionTests { "isBase64Encoded": false } """ - + let data = albEventWithLowercaseHeaders.data(using: .utf8)! let albRequest = try JSONDecoder().decode(ALBTargetGroupRequest.self, from: data) - + let httpRequest = try albRequest.httpRequest() - + #expect(httpRequest.scheme == "https") #expect(httpRequest.authority == "lambda-alb-123578498.us-east-1.elb.amazonaws.com") } - + @Test("HTTPResponse to ALB response conversion") func testHTTPResponseToALBResponse() throws { var httpResponse = HTTPResponse(status: .ok) httpResponse.headerFields[HTTPField.Name.contentType] = "application/json" httpResponse.headerFields[HTTPField.Name.contentLength] = "42" - + let albResponse = ALBTargetGroupResponse(from: httpResponse) - + #expect(albResponse.statusCode == .ok) #expect(albResponse.headers?[HTTPField.Name.contentType.rawName] == "application/json") #expect(albResponse.headers?[HTTPField.Name.contentLength.rawName] == "42") #expect(albResponse.isBase64Encoded == false) } - -} \ No newline at end of file + +} diff --git a/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift index 67a79b2..c90d9f6 100644 --- a/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift +++ b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift @@ -20,7 +20,7 @@ import Testing @testable import OpenAPILambda struct APIGatewayV2ConversionTests { - + static let apiGatewayEventJSON = """ { "rawQueryString": "", @@ -53,7 +53,7 @@ struct APIGatewayV2ConversionTests { "rawPath": "/stocks/AAPL" } """ - + static let apiGatewayEventWithQueryJSON = """ { "rawQueryString": "limit=10&offset=0", @@ -83,14 +83,14 @@ struct APIGatewayV2ConversionTests { "rawPath": "/stocks/AAPL" } """ - + @Test("API Gateway v2 request to HTTPRequest conversion") func testAPIGatewayV2RequestToHTTPRequest() throws { let data = APIGatewayV2ConversionTests.apiGatewayEventJSON.data(using: .utf8)! let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) - + let httpRequest = try apiGatewayRequest.httpRequest() - + #expect(httpRequest.method == HTTPRequest.Method.get) #expect(httpRequest.path == "/stocks/AAPL") #expect(httpRequest.scheme == "https") @@ -99,29 +99,29 @@ struct APIGatewayV2ConversionTests { #expect(httpRequest.headerFields[HTTPField.Name.userAgent] == "curl/8.1.2") #expect(httpRequest.headerFields[HTTPField.Name.authorization] == "Bearer 123") } - + @Test("API Gateway v2 request with query string") func testAPIGatewayV2RequestWithQueryString() throws { let data = APIGatewayV2ConversionTests.apiGatewayEventWithQueryJSON.data(using: .utf8)! let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: data) - + let httpRequest = try apiGatewayRequest.httpRequest() - + #expect(httpRequest.path == "/stocks/AAPL?limit=10&offset=0") } - + @Test("HTTPResponse to API Gateway v2 response conversion") func testHTTPResponseToAPIGatewayV2Response() throws { var httpResponse = HTTPResponse(status: .ok) httpResponse.headerFields[HTTPField.Name.contentType] = "application/json" httpResponse.headerFields[HTTPField.Name.contentLength] = "42" - + let apiGatewayResponse = APIGatewayV2Response(from: httpResponse) - + #expect(apiGatewayResponse.statusCode == .ok) #expect(apiGatewayResponse.headers?[HTTPField.Name.contentType.rawName] == "application/json") #expect(apiGatewayResponse.headers?[HTTPField.Name.contentLength.rawName] == "42") #expect(apiGatewayResponse.isBase64Encoded == false) #expect(apiGatewayResponse.cookies == nil) } -} \ No newline at end of file +} diff --git a/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift index 08a5dfd..e8546e3 100644 --- a/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift +++ b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift @@ -20,29 +20,29 @@ import Testing @Suite("HTTP Headers Conversion Tests") struct HTTPHeadersConversionTests { - + @Test("Multi-value headers preserved as comma-separated") func testMultiValueHeadersPreserved() throws { var httpResponse = HTTPResponse(status: .ok) httpResponse.headerFields.append(HTTPField(name: HTTPField.Name.setCookie, value: "session=abc123")) httpResponse.headerFields.append(HTTPField(name: HTTPField.Name.setCookie, value: "theme=dark")) - + let albResponse = ALBTargetGroupResponse(from: httpResponse) - + #expect(albResponse.headers?[HTTPField.Name.setCookie.rawName] == "session=abc123, theme=dark") #expect(albResponse.multiValueHeaders == nil) } - + @Test("HTTPHeaders to HTTPFields conversion") func testHTTPHeadersToHTTPFields() throws { let headers: HTTPHeaders = [ "Set-Cookie": "session=abc123, theme=dark", - "Content-Type": "application/json" + "Content-Type": "application/json", ] - + let httpFields = headers.httpFields() - + #expect(httpFields[HTTPField.Name.setCookie] == "session=abc123, theme=dark") #expect(httpFields[HTTPField.Name.contentType] == "application/json") } -} \ No newline at end of file +} From fd235f16e448e48d5d33d45f0b60d945613f3ac3 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 25 Oct 2025 12:12:57 +0200 Subject: [PATCH 7/7] fix license header --- Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift | 2 +- Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift | 2 +- Sources/EventSource/ALB/OpenAPILambdaALB.swift | 2 +- Sources/EventSource/HTTPHeadersExtension.swift | 2 +- Tests/OpenAPILambdaTests/ALBConversionTests.swift | 2 +- Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift | 2 +- Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift index 246aa99..c53e817 100644 --- a/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift +++ b/Examples/quoteapi-alb/Sources/QuoteAPI/QuoteService.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift index 1e70483..4b5d8c6 100644 --- a/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift +++ b/Sources/EventSource/ALB/ALBTargetGroup+HTTPRequest.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/EventSource/ALB/OpenAPILambdaALB.swift b/Sources/EventSource/ALB/OpenAPILambdaALB.swift index 5e26373..5bdd07c 100644 --- a/Sources/EventSource/ALB/OpenAPILambdaALB.swift +++ b/Sources/EventSource/ALB/OpenAPILambdaALB.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/EventSource/HTTPHeadersExtension.swift b/Sources/EventSource/HTTPHeadersExtension.swift index 3303533..b882fbf 100644 --- a/Sources/EventSource/HTTPHeadersExtension.swift +++ b/Sources/EventSource/HTTPHeadersExtension.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/OpenAPILambdaTests/ALBConversionTests.swift b/Tests/OpenAPILambdaTests/ALBConversionTests.swift index 169baad..85e89a9 100644 --- a/Tests/OpenAPILambdaTests/ALBConversionTests.swift +++ b/Tests/OpenAPILambdaTests/ALBConversionTests.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift index c90d9f6..e10a7f2 100644 --- a/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift +++ b/Tests/OpenAPILambdaTests/APIGatewayV2ConversionTests.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift index e8546e3..ffdea69 100644 --- a/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift +++ b/Tests/OpenAPILambdaTests/HTTPHeadersConversionTests.swift @@ -3,7 +3,7 @@ // This source file is part of the Swift OpenAPI Lambda open source project // // Copyright Swift OpenAPI Lambda project authors -// Copyright (c) 2023 Amazon.com, Inc. or its affiliates. +// Copyright (c) 2025 Amazon.com, Inc. or its affiliates. // Licensed under Apache License v2.0 // // See LICENSE.txt for license information