Skip to content

Add LambdaAPI Example of a Serverless REST API #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
15 changes: 15 additions & 0 deletions Examples/LambdaFunctions/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ let package = Package(
.executable(name: "APIGateway", targets: ["APIGateway"]),
// fully featured example with domain specific business logic
.executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomerd / @fabianfett we need to remove all executables from the products. If you export it as a product, then other libraries can depend on them. swift run works on all targets, so just leave out everything that's not meant for other packages to depend on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @fabianfett for pointing out to me that this isn't the main Package.swift. So we don't need to remove those here. They still aren't necessary but not an issue to have them.

// Full REST API Example using APIGateway, Lambda, DynamoDB
.executable(name: "ProductAPI", targets: ["ProductAPI"]),
],
dependencies: [
// this is the dependency on the swift-aws-lambda-runtime library
// in real-world projects this would say
// .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0")
.package(name: "swift-aws-lambda-runtime", path: "../.."),

// The following packages are required by LambdaAPI
// AWS SDK Swift
.package(url: "https://github.com/swift-aws/aws-sdk-swift.git", from: "5.0.0-alpha.3"),
// Logging
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
],
targets: [
.target(name: "HelloWorld", dependencies: [
Expand All @@ -42,5 +50,12 @@ let package = Package(
.target(name: "CurrencyExchange", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
]),
.target(name: "ProductAPI", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
.product(name: "AWSDynamoDB", package: "aws-sdk-swift"),
.product(name: "Logging", package: "swift-log"),
]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSLambdaEvents
import class Foundation.JSONEncoder
import class Foundation.JSONDecoder

extension APIGateway.V2.Request {

static private let decoder = JSONDecoder()

public func bodyObject<T: Codable>() throws -> T {
guard let body = self.body,
let dataBody = body.data(using: .utf8)
else {
throw APIError.invalidRequest
}
return try Self.decoder.decode(T.self, from: dataBody)
}
}

extension APIGateway.V2.Response {

private static let encoder = JSONEncoder()

public static let defaultHeaders = [
"Content-Type": "application/json",
//Security warning: XSS are enabled
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS,GET,POST,PUT,DELETE",
"Access-Control-Allow-Credentials": "true",
]

public init(with error: Error, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
self.init(
statusCode: statusCode,
headers: APIGateway.V2.Response.defaultHeaders,
multiValueHeaders: nil,
body: "{\"message\":\"\(String(describing: error))\"}",
isBase64Encoded: false
)
}

public init<Out: Encodable>(with object: Out, statusCode: AWSLambdaEvents.HTTPResponseStatus) {
var body: String = "{}"
if let data = try? Self.encoder.encode(object) {
body = String(data: data, encoding: .utf8) ?? body
}
self.init(
statusCode: statusCode,
headers: APIGateway.V2.Response.defaultHeaders,
multiValueHeaders: nil,
body: body,
isBase64Encoded: false
)
}
}
57 changes: 57 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/Product+DynamoDB.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSDynamoDB
import Foundation

extension Product {

public var dynamoDictionary: [String: DynamoDB.AttributeValue] {
var dictionary = [
Field.sku: DynamoDB.AttributeValue(s: sku),
Field.name: DynamoDB.AttributeValue(s: name),
Field.description: DynamoDB.AttributeValue(s: description),
]
if let createdAt = createdAt?.timeIntervalSince1970String {
dictionary[Field.createdAt] = DynamoDB.AttributeValue(n: createdAt)
}

if let updatedAt = updatedAt?.timeIntervalSince1970String {
dictionary[Field.updatedAt] = DynamoDB.AttributeValue(n: updatedAt)
}
return dictionary
}

public init(dictionary: [String: DynamoDB.AttributeValue]) throws {
guard let name = dictionary[Field.name]?.s,
let sku = dictionary[Field.sku]?.s,
let description = dictionary[Field.description]?.s
else {
throw APIError.invalidItem
}
self.name = name
self.sku = sku
self.description = description
if let value = dictionary[Field.createdAt]?.n,
let timeInterval = TimeInterval(value) {
let date = Date(timeIntervalSince1970: timeInterval)
self.createdAt = date.iso8601
}
if let value = dictionary[Field.updatedAt]?.n,
let timeInterval = TimeInterval(value) {
let date = Date(timeIntervalSince1970: timeInterval)
self.updatedAt = date.iso8601
}
}
}
31 changes: 31 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/Product.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation

public struct Product: Codable {
public let sku: String
public let name: String
public let description: String
public var createdAt: String?
public var updatedAt: String?

public struct Field {
static let sku = "sku"
static let name = "name"
static let description = "description"
static let createdAt = "createdAt"
static let updatedAt = "updatedAt"
}
}
101 changes: 101 additions & 0 deletions Examples/LambdaFunctions/Sources/ProductAPI/ProductLambda.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSDynamoDB
import AWSLambdaEvents
import AWSLambdaRuntime
import AsyncHTTPClient
import Logging
import NIO

enum Operation: String {
case create
case read
case update
case delete
case list
}

struct EmptyResponse: Codable {}

struct ProductLambda: LambdaHandler {

typealias In = APIGateway.V2.Request
typealias Out = APIGateway.V2.Response

let dbTimeout: Int64 = 30
let region: Region
let db: AWSDynamoDB.DynamoDB
let service: ProductService
let tableName: String
let operation: Operation
var httpClient: HTTPClient

static func currentRegion() -> Region {
if let awsRegion = Lambda.env("AWS_REGION") {
let value = Region(rawValue: awsRegion)
return value
} else {
return .useast1
}
}

static func tableName() throws -> String {
guard let tableName = Lambda.env("PRODUCTS_TABLE_NAME") else {
throw APIError.tableNameNotFound
}
return tableName
}

init(context: Lambda.InitializationContext) throws {

guard let handler = Lambda.env("_HANDLER"),
let operation = Operation(rawValue: handler) else {
throw APIError.invalidHandler
}
self.operation = operation
self.region = Self.currentRegion()

let lambdaRuntimeTimeout: TimeAmount = .seconds(dbTimeout)
let timeout = HTTPClient.Configuration.Timeout(
connect: lambdaRuntimeTimeout,
read: lambdaRuntimeTimeout
)

let configuration = HTTPClient.Configuration(timeout: timeout)
self.httpClient = HTTPClient(
eventLoopGroupProvider: .shared(context.eventLoop),
configuration: configuration
)

self.db = AWSDynamoDB.DynamoDB(region: region, httpClientProvider: .shared(self.httpClient))
self.tableName = try Self.tableName()

self.service = ProductService(
db: db,
tableName: tableName
)
}

func handle(
context: Lambda.Context, event: APIGateway.V2.Request,
callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void
) {
let _ = ProductLambdaHandler(service: service, operation: operation)
.handle(context: context, event: event)
.always { (result) in
callback(result)
}
}
}
Loading