Skip to content

Commit 21ccb0d

Browse files
Add LambdaAPI Example of a Serverless REST API
1 parent 0535cb7 commit 21ccb0d

File tree

11 files changed

+1020
-0
lines changed

11 files changed

+1020
-0
lines changed

Examples/LambdaFunctions/Package.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ let package = Package(
1818
.executable(name: "APIGateway", targets: ["APIGateway"]),
1919
// fully featured example with domain specific business logic
2020
.executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]),
21+
// Full REST API Example using APIGateway, Lambda, DynamoDB
22+
.executable(name: "LambdaAPI", targets: ["LambdaAPI"]),
2123
],
2224
dependencies: [
2325
// this is the dependency on the swift-aws-lambda-runtime library
2426
// in real-world projects this would say
2527
// .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0")
2628
.package(name: "swift-aws-lambda-runtime", path: "../.."),
29+
30+
// The following packages are required by LambdaAPI
31+
// AWS SDK Swift
32+
.package(url: "https://github.com/swift-aws/aws-sdk-swift.git", from: "5.0.0-alpha.3"),
33+
// Logging
34+
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2735
],
2836
targets: [
2937
.target(name: "HelloWorld", dependencies: [
@@ -42,5 +50,12 @@ let package = Package(
4250
.target(name: "CurrencyExchange", dependencies: [
4351
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
4452
]),
53+
.target(name: "LambdaAPI", dependencies: [
54+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
55+
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
56+
.product(name: "AWSDynamoDB", package: "aws-sdk-swift"),
57+
.product(name: "Logging", package: "swift-log"),
58+
]
59+
)
4560
]
4661
)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import AWSLambdaEvents
17+
18+
public extension APIGateway.V2.Request {
19+
func object<T: Codable>() throws -> T {
20+
let decoder = JSONDecoder()
21+
guard let body = self.body,
22+
let dataBody = body.data(using: .utf8) else {
23+
throw APIError.invalidRequest
24+
}
25+
return try decoder.decode(T.self, from: dataBody)
26+
}
27+
}
28+
29+
let defaultHeaders = ["Content-Type": "application/json",
30+
"Access-Control-Allow-Origin": "*",
31+
"Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE",
32+
"Access-Control-Allow-Credentials": "true"]
33+
34+
extension APIGateway.V2.Response {
35+
36+
init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
37+
38+
self.init(
39+
statusCode: statusCode,
40+
headers: defaultHeaders,
41+
multiValueHeaders: nil,
42+
body: "{\"message\":\"\(error.localizedDescription)\"}",
43+
isBase64Encoded: false
44+
)
45+
}
46+
47+
init<Out: Encodable>(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
48+
let encoder = JSONEncoder()
49+
50+
var body: String = "{}"
51+
if let data = try? encoder.encode(object) {
52+
body = String(data: data, encoding: .utf8) ?? body
53+
}
54+
self.init(
55+
statusCode: statusCode,
56+
headers: defaultHeaders,
57+
multiValueHeaders: nil,
58+
body: body,
59+
isBase64Encoded: false
60+
)
61+
62+
}
63+
64+
init<Out: Encodable>(with result: Result<Out, Error>, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
65+
66+
switch result {
67+
case .success(let value):
68+
self.init(with: value, statusCode: statusCode)
69+
case .failure(let error):
70+
self.init(with: error, statusCode: statusCode)
71+
}
72+
}
73+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import AWSDynamoDB
17+
18+
public struct ProductField {
19+
static let sku = "sku"
20+
static let name = "name"
21+
static let description = "description"
22+
static let createdAt = "createdAt"
23+
static let updatedAt = "updatedAt"
24+
}
25+
26+
public extension Product {
27+
var dynamoDictionary: [String : DynamoDB.AttributeValue] {
28+
var dictionary = [ProductField.sku: DynamoDB.AttributeValue(s:sku),
29+
ProductField.name: DynamoDB.AttributeValue(s:name),
30+
ProductField.description: DynamoDB.AttributeValue(s:description)]
31+
if let createdAt = createdAt {
32+
dictionary[ProductField.createdAt] = DynamoDB.AttributeValue(s:createdAt)
33+
}
34+
35+
if let updatedAt = updatedAt {
36+
dictionary[ProductField.updatedAt] = DynamoDB.AttributeValue(s:updatedAt)
37+
}
38+
return dictionary
39+
}
40+
41+
init(dictionary: [String: DynamoDB.AttributeValue]) throws {
42+
guard let name = dictionary[ProductField.name]?.s,
43+
let sku = dictionary[ProductField.sku]?.s,
44+
let description = dictionary[ProductField.description]?.s else {
45+
throw APIError.invalidItem
46+
}
47+
self.name = name
48+
self.sku = sku
49+
self.description = description
50+
self.createdAt = dictionary[ProductField.createdAt]?.s
51+
self.updatedAt = dictionary[ProductField.updatedAt]?.s
52+
}
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
public struct Product: Codable {
18+
public let sku: String
19+
public let name: String
20+
public let description: String
21+
public var createdAt: String?
22+
public var updatedAt: String?
23+
}
24+
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
#if canImport(FoundationNetworking)
17+
import FoundationNetworking
18+
#endif
19+
import AWSLambdaRuntime
20+
import AWSDynamoDB
21+
import NIO
22+
import Logging
23+
import AsyncHTTPClient
24+
import AWSLambdaEvents
25+
26+
enum Operation: String {
27+
case create
28+
case read
29+
case update
30+
case delete
31+
case list
32+
case unknown
33+
}
34+
35+
struct EmptyResponse: Codable {
36+
37+
}
38+
39+
struct ProductLambda: LambdaHandler {
40+
41+
//typealias In = APIGateway.SimpleRequest
42+
typealias In = APIGateway.V2.Request
43+
typealias Out = APIGateway.V2.Response
44+
45+
let dbTimeout:Int64 = 30
46+
47+
let region: Region
48+
let db: AWSDynamoDB.DynamoDB
49+
let service: ProductService
50+
let tableName: String
51+
let operation: Operation
52+
53+
var httpClient: HTTPClient
54+
55+
static func currentRegion() -> Region {
56+
57+
if let awsRegion = ProcessInfo.processInfo.environment["AWS_REGION"] {
58+
let value = Region(rawValue: awsRegion)
59+
return value
60+
61+
} else {
62+
//Default configuration
63+
return .useast1
64+
}
65+
}
66+
67+
static func tableName() throws -> String {
68+
guard let tableName = ProcessInfo.processInfo.environment["PRODUCTS_TABLE_NAME"] else {
69+
throw APIError.tableNameNotFound
70+
}
71+
return tableName
72+
}
73+
74+
init(eventLoop: EventLoop) {
75+
76+
let handler = Lambda.env("_HANDLER") ?? ""
77+
self.operation = Operation(rawValue: handler) ?? .unknown
78+
79+
self.region = Self.currentRegion()
80+
logger.info("\(Self.currentRegion())")
81+
82+
let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout)
83+
let timeout = HTTPClient.Configuration.Timeout(connect: lambdaRuntimeTimeout,
84+
read: lambdaRuntimeTimeout)
85+
let configuration = HTTPClient.Configuration(timeout: timeout)
86+
self.httpClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: configuration)
87+
88+
self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient))
89+
logger.info("DynamoDB")
90+
91+
self.tableName = (try? Self.tableName()) ?? ""
92+
93+
self.service = ProductService(
94+
db: db,
95+
tableName: tableName
96+
)
97+
logger.info("ProductService")
98+
}
99+
100+
func handle(context: Lambda.Context, event: APIGateway.V2.Request, callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void) {
101+
let _ = ProductLambdaHandler(service: service, operation: operation).handle(context: context, event: event)
102+
.always { (result) in
103+
callback(result)
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)