Add quote api example (#20)

In preparation for a v2 release that will support the Lambda Runtime v2, I'm adding an end-to-end example project
This commit is contained in:
Sébastien Stormacq
2025-08-30 22:31:12 +02:00
committed by GitHub
parent 78dcf0c864
commit 6a54484782
13 changed files with 578 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
/.aws-sam
/.build
/.swiftpm
/.vscode
Package.resolved
samconfig.toml
*.d
*o
*swiftdeps
+3
View File
@@ -0,0 +1,3 @@
# image used to compile your Swift code
FROM public.ecr.aws/docker/library/swift:5.9.1-amazonlinux2
RUN yum -y install git jq tar zip openssl-devel
+62
View File
@@ -0,0 +1,62 @@
### Add functions here and link them to builder-bot format MUST BE "build-FunctionResourceName in template.yaml"
build-QuoteService: builder-bot
# Helper commands
build:
sam build
deploy:
sam deploy
logs:
sam logs --stack-name QuoteService --name QuoteService
tail:
sam logs --stack-name QuoteService --name QuoteService --tail
local:
LOCAL_LAMBDA_SERVER_ENABLED=true swift run QuoteService
invoke:
curl -v -H 'Authorization: 123' https://k3lbszo7x6.execute-api.us-east-1.amazonaws.com/stocks/AAPL
###################### 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)
## 1. git clone https://github.com/swift-server/swift-openapi-lambda ..
## 2. Change `Package.swift` dependency to ../swift-openapi-lambda
## 3. add /.. to BUILD_SRC
## $(eval $@BUILD_SRC = $(PWD)/..)
$(eval $@BUILD_SRC = $(PWD))
## 4. add `cd quoteapi &&` to the docker BUILD_CMD
## $(eval $@BUILD_CMD = "ls && cd quoteapi && 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 app from stage to artifacts dir
cp $($@STAGE)/* $($@ARTIFACTS_DIR)
+44
View File
@@ -0,0 +1,44 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "QuoteService",
platforms: [
.macOS(.v13), .iOS(.v15), .tvOS(.v15), .watchOS(.v6),
],
products: [
.executable(name: "QuoteService", targets: ["QuoteService"])
],
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.5.0"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.3"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.4.0"),
.package(url: "https://github.com/swift-server/swift-openapi-lambda.git", from: "0.2.0"),
// .package(name: "swift-openapi-lambda", path: "../swift-openapi-lambda")
],
targets: [
.executableTarget(
name: "QuoteService",
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",
resources: [
.copy("openapi.yaml"),
.copy("openapi-generator-config.yaml"),
],
plugins: [
.plugin(
name: "OpenAPIGenerator",
package: "swift-openapi-generator"
)
]
)
]
)
+79
View File
@@ -0,0 +1,79 @@
# QuoteAPI
This application illustrates how to deploy a Server-Side Swift workload on AWS using the [AWS Serverless Application Model (SAM)](https://aws.amazon.com/serverless/sam/) toolkit. The workload is a simple REST API that returns a string from an Amazon API Gateway. Requests to the API Gateway endpoint are handled by an AWS Lambda Function written in Swift.
## 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
```
## Deploy the application
The **sam deploy** command creates the Lambda function and API Gateway in your AWS account.
```bash
sam deploy --guided
```
Accept the default response to every prompt, except the following warning:
```bash
QuoteService may not have authorization defined, Is this okay? [y/N]: y
```
The project creates a publicly accessible API endpoint. This is a warning to inform you the API does not have authorization. If you are interested in adding authorization to the API, please refer to the [SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-httpapi.html).
## Use the API
At the end of the deployment, SAM displays the endpoint of your API Gateway:
```bash
Outputs
----------------------------------------------------------------------------------------
Key SwiftAPIEndpoint
Description API Gateway endpoint URL for your application
Value https://[your-api-id].execute-api.[your-aws-region].amazonaws.com
----------------------------------------------------------------------------------------
```
Use cURL or a tool such as [Postman](https://www.postman.com/) to interact with your API. Replace **[your-api-endpoint]** with the SwiftAPIEndpoint value from the deployment output.
**Invoke the API Endpoint**
```bash
curl https://[your-api-endpoint]/stocks/AMZN
```
## Test the API Locally
SAM also allows you to execute your Lambda functions locally on your development computer. Follow these instructions to execute the Lambda function locally. Further capabilities can be explored in the [SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-invoke.html).
**Event Files**
When a Lambda function is invoked, API Gateway sends an event to the function with all the data packaged with the API call. When running the functions locally, you pass in a json file to the function that simulates the event data. The **events** folder contains a json file for the function.
**Invoke the Lambda Function Locally**
```bash
sam local invoke QuoteService --event events/GetQuote.json
```
## 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
```
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
//
// 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 OpenAPIRuntime
import OpenAPILambda
@main
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi {
init(transport: OpenAPILambdaTransport) throws {
try self.registerHandlers(on: transport)
}
func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {
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
)
return .ok(.init(body: .json(price)))
}
}
@@ -0,0 +1,4 @@
generate:
- types
- server
+54
View File
@@ -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
+34
View File
@@ -0,0 +1,34 @@
{
"rawQueryString": "",
"headers": {
"host": "b2k1t8fon7.execute-api.us-east-1.amazonaws.com",
"x-forwarded-port": "443",
"content-length": "0",
"x-amzn-trace-id": "Root=1-6571d134-63dbe8ee21efa87555d59265",
"x-forwarded-for": "191.95.148.219",
"x-forwarded-proto": "https",
"accept": "*/*",
"user-agent": "curl/8.1.2"
},
"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"
}
+61
View File
@@ -0,0 +1,61 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for QuoteService
Globals:
Function:
Timeout: 60
CodeUri: .
Handler: swift.bootstrap
Runtime: provided.al2
MemorySize: 512
Architectures:
- arm64
Resources:
# Lambda function
QuoteService:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
# by default, AWS Lambda runtime produces no log
# use `LOG_LEVEL: debug` for for lifecycle and event handling information
# use `LOG_LEVEL: trace` for detailed input event information
LOG_LEVEL: trace
Events:
# pass through all HTTP verbs and paths
Api:
Type: HttpApi
Properties:
ApiId: !Ref MyProtectedApi
Path: /{proxy+}
Method: ANY
Auth:
Authorizer: MyLambdaAuthorizer
Metadata:
BuildMethod: makefile
MyProtectedApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
DefaultAuthorizer: MyLambdaAuthorizer
Authorizers:
MyLambdaAuthorizer:
AuthorizerPayloadFormatVersion: 2.0
EnableFunctionDefaultPermissions: true
EnableSimpleResponses: true
FunctionArn: arn:aws:lambda:us-east-1:486652066693:function:LambdaAuthorizer-LambdaAuthorizer-TSH4AsHiqICi
Identity:
Headers:
- Authorization
# print API endpoint
Outputs:
SwiftAPIEndpoint:
Description: "API Gateway endpoint URL for your application"
Value: !Sub "https://${MyProtectedApi}.execute-api.${AWS::Region}.amazonaws.com"
# Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## 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
##
##===----------------------------------------------------------------------===##
# ===----------------------------------------------------------------------===//
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2024 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ===----------------------------------------------------------------------===//
set -euo pipefail
log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
if [[ -f .swiftformatignore ]]; then
log "Found swiftformatignore file..."
log "Running swift format format..."
tr '\n' '\0' < .swiftformatignore| xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
log "Running swift format lint..."
tr '\n' '\0' < .swiftformatignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
else
log "Running swift format format..."
git ls-files -z '*.swift' | xargs -0 swift format format --parallel --in-place
log "Running swift format lint..."
git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
fi
log "Checking for modified files..."
GIT_PAGER='' git diff --exit-code '*.swift'
log "✅ Found no formatting issues."
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
##===----------------------------------------------------------------------===##
##
## 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
##
##===----------------------------------------------------------------------===##
# ===----------------------------------------------------------------------===//
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2024 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
#
# ===----------------------------------------------------------------------===//
set -euo pipefail
set +x
log() { printf -- "** %s\n" "$*" >&2; }
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
fatal() { error "$@"; exit 1; }
test -n "${PROJECT_NAME:-}" || fatal "PROJECT_NAME unset"
if [ -f .license_header_template ]; then
# allow projects to override the license header template
expected_file_header_template=$(cat .license_header_template)
else
expected_file_header_template="@@===----------------------------------------------------------------------===@@
@@
@@ This source file is part of the ${PROJECT_NAME} open source project
@@
@@ Copyright (c) YEARS Apple Inc. and the ${PROJECT_NAME} project authors
@@ Licensed under Apache License v2.0
@@
@@ See LICENSE.txt for license information
@@ See CONTRIBUTORS.txt for the list of ${PROJECT_NAME} project authors
@@
@@ SPDX-License-Identifier: Apache-2.0
@@
@@===----------------------------------------------------------------------===@@"
fi
paths_with_missing_license=( )
# file_excludes=".license_header_template
# .licenseignore"
# if [ -f .licenseignore ]; then
# file_excludes=$(printf '%s\n%s' "$file_excludes" "$(cat .licenseignore)")
# fi
# file_paths=$(echo "$file_excludes" | tr '\n' '\0' | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files)
file_paths=$(tr '\n' '\0' < .licenseignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files ":(exclude).licenseignore" ":(exclude).license_header_template" )
echo $file_paths
while IFS= read -r file_path; do
file_basename=$(basename -- "${file_path}")
file_extension="${file_basename##*.}"
# shellcheck disable=SC2001 # We prefer to use sed here instead of bash search/replace
case "${file_extension}" in
swift) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
h) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
c) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
sh) expected_file_header=$(cat <(echo '#!/bin/bash') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;;
kts) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
gradle) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
groovy) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
java) expected_file_header=$(sed -e 's|@@|//|g' <<<"${expected_file_header_template}") ;;
py) expected_file_header=$(cat <(echo '#!/usr/bin/env python3') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;;
rb) expected_file_header=$(cat <(echo '#!/usr/bin/env ruby') <(sed -e 's|@@|##|g' <<<"${expected_file_header_template}")) ;;
in) expected_file_header=$(sed -e 's|@@|##|g' <<<"${expected_file_header_template}") ;;
cmake) expected_file_header=$(sed -e 's|@@|##|g' <<<"${expected_file_header_template}") ;;
*)
error "Unsupported file extension ${file_extension} for file (exclude or update this script): ${file_path}"
paths_with_missing_license+=("${file_path} ")
;;
esac
expected_file_header_linecount=$(wc -l <<<"${expected_file_header}")
file_header=$(head -n "${expected_file_header_linecount}" "${file_path}")
normalized_file_header=$(
echo "${file_header}" \
| sed -e 's/20[12][0123456789]-20[12][0123456789]/YEARS/' -e 's/20[12][0123456789]/YEARS/' \
)
if ! diff -u \
--label "Expected header" <(echo "${expected_file_header}") \
--label "${file_path}" <(echo "${normalized_file_header}")
then
paths_with_missing_license+=("${file_path} ")
fi
done <<< "$file_paths"
if [ "${#paths_with_missing_license[@]}" -gt 0 ]; then
fatal "❌ Found missing license header in files: ${paths_with_missing_license[*]}."
fi
log "✅ Found no files with missing license header."
+7
View File
@@ -0,0 +1,7 @@
extends: default
rules:
line-length: false
document-start: false
truthy:
check-keys: false # Otherwise we get a false positive on GitHub action's `on` key