Skip to content

Commit 4eaa0fd

Browse files
Add Async Await with conditional compilation
1 parent 7fabfcc commit 4eaa0fd

File tree

8 files changed

+344
-45
lines changed

8 files changed

+344
-45
lines changed

Products/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import PackageDescription
66
let package = Package(
77
name: "swift-rest-api",
88
platforms: [
9-
.macOS(.v10_13),
9+
.macOS(.v12),
1010
],
1111
products: [
1212
// Products define the executables and libraries produced by a package, and make them visible to other packages.

Products/Sources/ProductService/ProductService.swift

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,97 @@ public class ProductService {
5959
self.db = db
6060
self.tableName = tableName
6161
}
62+
}
63+
64+
#if compiler(>=5.5) && canImport(_Concurrency)
65+
66+
public extension ProductService {
6267

63-
public func createItem(product: Product) -> EventLoopFuture<Product> {
68+
func createItem(product: Product) async throws -> Product {
69+
var product = product
70+
let date = Date()
71+
product.createdAt = date.iso8601
72+
product.updatedAt = date.iso8601
73+
let input = DynamoDB.PutItemCodableInput(item: product, tableName: tableName)
74+
75+
let _ = try await db.putItem(input)
76+
return try await readItem(key: product.sku)
77+
}
78+
79+
func readItem(key: String) async throws -> Product {
80+
let input = DynamoDB.GetItemInput(
81+
key: [Product.Field.sku: DynamoDB.AttributeValue.s(key)],
82+
tableName: tableName
83+
)
84+
let data = try await db.getItem(input, type: Product.self)
85+
guard let product = data.item else {
86+
throw ProductError.notFound
87+
}
88+
return product
89+
}
90+
91+
func updateItem(product: Product) async throws -> Product {
92+
var product = product
93+
let date = Date()
94+
let updatedAt = date.iso8601
95+
product.updatedAt = date.iso8601
96+
97+
let input = DynamoDB.UpdateItemInput(
98+
conditionExpression: "attribute_exists(#createdAt)",
99+
expressionAttributeNames: [
100+
"#name": Product.Field.name,
101+
"#description": Product.Field.description,
102+
"#updatedAt": Product.Field.updatedAt,
103+
"#createdAt": Product.Field.createdAt
104+
],
105+
expressionAttributeValues: [
106+
":name": DynamoDB.AttributeValue.s(product.name),
107+
":description": DynamoDB.AttributeValue.s(product.description),
108+
":updatedAt": DynamoDB.AttributeValue.s(updatedAt)
109+
],
110+
key: [Product.Field.sku: DynamoDB.AttributeValue.s(product.sku)],
111+
returnValues: DynamoDB.ReturnValue.allNew,
112+
tableName: tableName,
113+
updateExpression: "SET #name = :name, #description = :description, #updatedAt = :updatedAt"
114+
)
115+
let _ = try await db.updateItem(input)
116+
return try await readItem(key: product.sku)
117+
}
118+
119+
func deleteItem(key: String) async throws {
120+
let input = DynamoDB.DeleteItemInput(
121+
key: [Product.Field.sku: DynamoDB.AttributeValue.s(key)],
122+
tableName: tableName
123+
)
124+
let _ = try await db.deleteItem(input)
125+
return
126+
}
127+
128+
func listItems() async throws -> [Product] {
129+
let input = DynamoDB.ScanInput(tableName: tableName)
130+
let data = try await db.scan(input, type: Product.self)
131+
return data.items ?? []
132+
}
133+
}
134+
135+
#else
136+
137+
public extension ProductService {
138+
139+
func createItem(product: Product) -> EventLoopFuture<Product> {
64140

65141
var product = product
66142
let date = Date()
67143
product.createdAt = date.iso8601
68144
product.updatedAt = date.iso8601
69145
let input = DynamoDB.PutItemCodableInput(item: product, tableName: tableName)
70-
146+
71147
return db.putItem(input).flatMap { _ -> EventLoopFuture<Product> in
72148
return self.readItem(key: product.sku)
73149
}
74150
}
75151

76-
public func readItem(key: String) -> EventLoopFuture<Product> {
152+
func readItem(key: String) -> EventLoopFuture<Product> {
77153
let input = DynamoDB.GetItemInput(
78154
key: [Product.Field.sku: DynamoDB.AttributeValue.s(key)],
79155
tableName: tableName
@@ -86,7 +162,7 @@ public class ProductService {
86162
}
87163
}
88164

89-
public func updateItem(product: Product) -> EventLoopFuture<Product> {
165+
func updateItem(product: Product) -> EventLoopFuture<Product> {
90166
var product = product
91167
let date = Date()
92168
let updatedAt = date.iso8601
@@ -115,18 +191,20 @@ public class ProductService {
115191
}
116192
}
117193

118-
public func deleteItem(key: String) -> EventLoopFuture<Void> {
194+
func deleteItem(key: String) -> EventLoopFuture<Void> {
119195
let input = DynamoDB.DeleteItemInput(
120196
key: [Product.Field.sku: DynamoDB.AttributeValue.s(key)],
121197
tableName: tableName
122198
)
123199
return db.deleteItem(input).map { _ in Void() }
124200
}
125201

126-
public func listItems() -> EventLoopFuture<[Product]> {
202+
func listItems() -> EventLoopFuture<[Product]> {
127203
let input = DynamoDB.ScanInput(tableName: tableName)
128204
return db.scan(input, type: Product.self).flatMapThrowing { data -> [Product] in
129205
return data.items ?? []
130206
}
131207
}
132208
}
209+
210+
#endif
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2022 (c) Andrea Scuderi - https://github.com/swift-sprinter
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import SotoDynamoDB
16+
import AWSLambdaEvents
17+
import AWSLambdaRuntime
18+
import AsyncHTTPClient
19+
import Logging
20+
import NIO
21+
import ProductService
22+
23+
enum Operation: String {
24+
case create = "build/Products.create"
25+
case read = "build/Products.read"
26+
case update = "build/Products.update"
27+
case delete = "build/Products.delete"
28+
case list = "build/Products.list"
29+
}
30+
31+
struct EmptyResponse: Codable {}
32+
33+
#if compiler(>=5.5) && canImport(_Concurrency)
34+
35+
struct AsyncProductLambda: AsyncLambdaHandler {
36+
37+
typealias In = APIGateway.V2.Request
38+
typealias Out = APIGateway.V2.Response
39+
40+
let dbTimeout: Int64 = 30
41+
let region: Region
42+
let db: SotoDynamoDB.DynamoDB
43+
let service: ProductService
44+
let tableName: String
45+
let operation: Operation
46+
var httpClient: HTTPClient
47+
48+
static func currentRegion() -> Region {
49+
if let awsRegion = Lambda.env("AWS_REGION") {
50+
let value = Region(rawValue: awsRegion)
51+
return value
52+
} else {
53+
return .useast1
54+
}
55+
}
56+
57+
static func tableName() throws -> String {
58+
guard let tableName = Lambda.env("PRODUCTS_TABLE_NAME") else {
59+
throw APIError.tableNameNotFound
60+
}
61+
return tableName
62+
}
63+
64+
init(context: Lambda.InitializationContext) throws {
65+
66+
guard let handler = Lambda.env("_HANDLER"),
67+
let operation = Operation(rawValue: handler) else {
68+
throw APIError.invalidHandler
69+
}
70+
self.operation = operation
71+
self.region = Self.currentRegion()
72+
73+
let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout)
74+
let timeout = HTTPClient.Configuration.Timeout(
75+
connect: lambdaRuntimeTimeout,
76+
read: lambdaRuntimeTimeout
77+
)
78+
79+
let configuration = HTTPClient.Configuration(timeout: timeout)
80+
self.httpClient = HTTPClient(
81+
eventLoopGroupProvider: .shared(context.eventLoop),
82+
configuration: configuration
83+
)
84+
85+
let awsClient = AWSClient(httpClientProvider: .shared(self.httpClient))
86+
self.db = SotoDynamoDB.DynamoDB(client: awsClient, region: region)
87+
self.tableName = try Self.tableName()
88+
89+
self.service = ProductService(
90+
db: db,
91+
tableName: tableName
92+
)
93+
}
94+
95+
func handle(event: AWSLambdaEvents.APIGateway.V2.Request, context: AWSLambdaRuntimeCore.Lambda.Context) async throws -> AWSLambdaEvents.APIGateway.V2.Response {
96+
return await AsyncProductLambdaHandler(service: service, operation: operation).handle(context: context, event: event)
97+
}
98+
}
99+
100+
#endif
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2022 (c) Andrea Scuderi - https://github.com/swift-sprinter
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
#if canImport(FoundationNetworking)
17+
import FoundationNetworking
18+
#endif
19+
import AWSLambdaRuntime
20+
import NIO
21+
import ProductService
22+
import Logging
23+
import AWSLambdaEvents
24+
25+
import AWSLambdaEvents
26+
import AWSLambdaRuntime
27+
import Logging
28+
import NIO
29+
30+
#if compiler(>=5.5) && canImport(_Concurrency)
31+
32+
struct AsyncProductLambdaHandler {
33+
34+
typealias In = APIGateway.V2.Request
35+
typealias Out = APIGateway.V2.Response
36+
37+
let service: ProductService
38+
let operation: Operation
39+
40+
func handle(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
41+
42+
switch self.operation {
43+
case .create:
44+
return await createLambdaHandler(context: context, event: event)
45+
case .read:
46+
return await readLambdaHandler(context: context, event: event)
47+
case .update:
48+
return await updateLambdaHandler(context: context, event: event)
49+
case .delete:
50+
return await deleteUpdateLambdaHandler(context: context, event: event)
51+
case .list:
52+
return await listUpdateLambdaHandler(context: context, event: event)
53+
}
54+
}
55+
56+
func createLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
57+
guard let product: Product = try? event.bodyObject() else {
58+
let error = APIError.invalidRequest
59+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
60+
}
61+
do {
62+
let result = try await service.createItem(product: product)
63+
return APIGateway.V2.Response(with: result, statusCode: .created)
64+
} catch {
65+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
66+
}
67+
}
68+
69+
func readLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
70+
guard let sku = event.pathParameters?["sku"] else {
71+
let error = APIError.invalidRequest
72+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
73+
}
74+
do {
75+
let result = try await service.readItem(key: sku)
76+
return APIGateway.V2.Response(with: result, statusCode: .ok)
77+
} catch {
78+
return APIGateway.V2.Response(with: error, statusCode: .notFound)
79+
}
80+
}
81+
82+
func updateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
83+
guard let product: Product = try? event.bodyObject() else {
84+
let error = APIError.invalidRequest
85+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
86+
}
87+
do {
88+
let result = try await service.updateItem(product: product)
89+
return APIGateway.V2.Response(with: result, statusCode: .ok)
90+
} catch {
91+
return APIGateway.V2.Response(with: error, statusCode: .notFound)
92+
}
93+
}
94+
95+
func deleteUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
96+
guard let sku = event.pathParameters?["sku"] else {
97+
let error = APIError.invalidRequest
98+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
99+
}
100+
do {
101+
try await service.deleteItem(key: sku)
102+
return APIGateway.V2.Response(with: EmptyResponse(), statusCode: .ok)
103+
} catch {
104+
return APIGateway.V2.Response(with: error, statusCode: .notFound)
105+
}
106+
}
107+
108+
func listUpdateLambdaHandler(context: Lambda.Context, event: APIGateway.V2.Request) async -> APIGateway.V2.Response {
109+
do {
110+
let result = try await service.listItems()
111+
return APIGateway.V2.Response(with: result, statusCode: .ok)
112+
} catch {
113+
return APIGateway.V2.Response(with: error, statusCode: .forbidden)
114+
}
115+
}
116+
}
117+
118+
#endif

Products/Sources/Products/ProductLambda.swift

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,8 @@ import Logging
2020
import NIO
2121
import ProductService
2222

23-
enum Operation: String {
24-
case create = "build/Products.create"
25-
case read = "build/Products.read"
26-
case update = "build/Products.update"
27-
case delete = "build/Products.delete"
28-
case list = "build/Products.list"
29-
}
30-
31-
struct EmptyResponse: Codable {}
32-
23+
#if !(compiler(>=5.5) && canImport(_Concurrency))
24+
3325
struct ProductLambda: LambdaHandler {
3426

3527
typealias In = APIGateway.V2.Request
@@ -101,3 +93,5 @@ struct ProductLambda: LambdaHandler {
10193
}
10294
}
10395
}
96+
97+
#endif

0 commit comments

Comments
 (0)