Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.0"),
],
targets: [
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1"]),
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1", "NIOFoundationCompat"]),
.testTarget(name: "SwiftAwsLambdaTests", dependencies: ["SwiftAwsLambda"]),
// samples
.target(name: "SwiftAwsLambdaSample", dependencies: ["SwiftAwsLambda"]),
Expand Down
149 changes: 107 additions & 42 deletions Sources/SwiftAwsLambda/Lambda+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,132 +13,197 @@
//===----------------------------------------------------------------------===//

import Foundation // for JSON
import NIO
import NIOFoundationCompat

/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` payloads.
/// This is the most common way to use this library in AWS Lambda, since its JSON based.
extension Lambda {
/// Run a Lambda defined by implementing the `LambdaCodableClosure` closure, having `In` and `Out` extending `Decodable` and `Encodable` respectively.
/// Run a Lambda defined by implementing the `CodableLambda.Closure` closure, having `In` and `Out` extending `Decodable` and `Encodable` respectively.
///
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
public static func run<In: Decodable, Out: Encodable>(_ closure: @escaping LambdaCodableClosure<In, Out>) {
self.run(LambdaClosureWrapper(closure))
public static func run<In: Decodable, Out: Encodable>(_ closure: @escaping CodableLambda.Closure<In, Out>) {
self.run(ClosureWrapper(closure))
}

/// Run a Lambda defined by implementing the `LambdaCodableHandler` protocol, having `In` and `Out` are `Decodable` and `Encodable` respectively.
/// Run a Lambda defined by implementing the `CodableLambdaHandler` protocol, having `In` and `Out` are `Decodable` and `Encodable` respectively.
///
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
public static func run<Handler>(_ handler: Handler) where Handler: LambdaCodableHandler {
self.run(handler as LambdaHandler)
public static func run<Handler>(_ handler: Handler) where Handler: CodableLambdaHandler {
self.run { _ in handler }
}

/// Run a Lambda defined by implementing the `CodableLambdaHandler` protocol, having `In` and `Out` are `Decodable` and `Encodable` respectively.
///
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
public static func run<Handler>(_ provider: @escaping (EventLoop) throws -> Handler) where Handler: CodableLambdaHandler {
self.run { try provider($0) as LambdaHandler }
}

// for testing
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping CodableLambda.Closure<In, Out>) -> Result<Int, Error> {
return self.run(provider: { _ in ClosureWrapper(closure) }, configuration: configuration)
}

// for testing
internal static func run<Handler>(handler: Handler, configuration: Configuration = .init()) -> LambdaLifecycleResult where Handler: LambdaCodableHandler {
internal static func run<Handler>(handler: Handler, configuration: Configuration = .init()) -> Result<Int, Error> where Handler: CodableLambdaHandler {
return self.run(handler: handler as LambdaHandler, configuration: configuration)
}

// for testing
internal static func run<Handler>(provider: @escaping (EventLoop) throws -> Handler, configuration: Configuration = .init()) -> Result<Int, Error> where Handler: CodableLambdaHandler {
return self.run(provider: { try provider($0) as LambdaHandler }, configuration: configuration)
}
}

/// A result type for a Lambda that returns a generic `Out`, having `Out` extend `Encodable`.
public typealias LambdaCodableResult<Out> = Result<Out, Error>
public enum CodableLambda {
/// A completion handler for a Lambda that returns a `Result<Out, Error>` result type.
public typealias CompletionHandler<Out> = (Result<Out, Error>?) -> Void

/// A callback for a Lambda that returns a `LambdaCodableResult<Out>` result type, having `Out` extend `Encodable`.
public typealias LambdaCodableCallback<Out> = (LambdaCodableResult<Out>) -> Void
/// A processing closure for a Lambda that takes a `String` and returns a `Result<Out, Error>` via `CompletionHandler` asynchronously.
public typealias Closure<In, Out> = (Lambda.Context, In, CompletionHandler<Out>) -> Void
}

/// A processing closure for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
/// A processing protocol for a Lambda that takes an `In` and returns an optional `Out`asynchronously via a `CompletionHandler<Out>` ,
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
public typealias LambdaCodableClosure<In, Out> = (LambdaContext, In, LambdaCodableCallback<Out>) -> Void
public protocol CodableLambdaHandler: LambdaHandler {
associatedtype In: Decodable
associatedtype Out: Encodable

/// A processing protocol for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
public protocol LambdaCodableHandler: LambdaHandler {
var codec: LambdaCodableCodec<In, Out> { get }

func handle(context: Lambda.Context, payload: In, callback: @escaping CodableLambda.CompletionHandler<Out>)
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to offer this as a protocol? I would have just implemented this as a convenience method on top of CodablePromiseLambdaHandler on Lambda. I'm unsure because the use case is hard for me to get:

On a full-control but more difficult to easy to use axis we have:

full-control <--------------------------------------------------------------> easy-to-use
CodablePromiseLambdaHandler                   CodableLambda.Closure<Out> direct on Lambda

And I'm not sure where the CodableLambdaHandler lands here? Is it the middle? Are Developers who are willing to use this not able to go all in on CodablePromiseLambdaHandler? My idea would be to maybe go with one full-control option and one batteries included option first. Since it's hard to deprecate APIs later, maybe fill in the middle later as needed/requested by the users.

}

/// A processing protocol for a Lambda that takes a `In` and returns an optional `Out` asynchronously via an `EventLoopPromise`.
public protocol CodablePromiseLambdaHandler: LambdaHandler {
associatedtype In: Decodable
associatedtype Out: Encodable

func handle(context: LambdaContext, payload: In, callback: @escaping LambdaCodableCallback<Out>)
var codec: LambdaCodableCodec<In, Out> { get }

func handle(context: Lambda.Context, payload: In, promise: EventLoopPromise<Out?>)
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if we want to go with an Out? here. If developers just handle a SNS.Message, they have to come up with some type that they will never return. That's why I would go with two protocols. One with an Out and one with none.

}

/// Default implementation for `LambdaCodableHandler` codec which uses JSON via `LambdaCodableJsonCodec`.
/// Default implementation for `CodableLambdaHandler` codec which uses JSON via `LambdaCodableJsonCodec`.
/// Advanced users that want to inject their own codec can do it by overriding this.
public extension LambdaCodableHandler {
public extension CodableLambdaHandler {
var codec: LambdaCodableCodec<In, Out> {
return LambdaCodableJsonCodec<In, Out>()
LambdaCodableJsonCodec<In, Out>()
}
}

/// LambdaCodableCodec is an abstract/empty implementation for codec which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding.
/// Default implementation for `CodableLambdaHandler` codec which uses JSON via `LambdaCodableJsonCodec`.
/// Advanced users that want to inject their own codec can do it by overriding this.
public extension CodablePromiseLambdaHandler {
Copy link
Member

Choose a reason for hiding this comment

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

Yes! +1

var codec: LambdaCodableCodec<In, Out> {
LambdaCodableJsonCodec<In, Out>()
}
}

/// LambdaCodableCodec is an abstract/empty implementation for codec which does `Encodable` -> `ByteBuffer` encoding and `ByteBuffer` -> `Decodable` decoding.
// TODO: would be nicer to use a protocol instead of this "abstract class", but generics get in the way
public class LambdaCodableCodec<In: Decodable, Out: Encodable> {
func encode(_: Out) -> Result<[UInt8], Error> { fatalError("not implmented") }
func decode(_: [UInt8]) -> Result<In, Error> { fatalError("not implmented") }
func encode(_: Out) -> Result<ByteBuffer, Error> { fatalError("not implmented") }
func decode(_: ByteBuffer) -> Result<In, Error> { fatalError("not implmented") }
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure, if we should stay with the Result here. I understand the personal preference 😉, but I don't know if this is in the interest of the platform/language.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ill try the other way

}

/// Default implementation of `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
public extension LambdaCodableHandler {
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping (LambdaResult) -> Void) {
/// Default implementation of `Encodable` -> `ByteBuffer` encoding and `ByteBuffer` -> `Decodable` decoding
public extension CodableLambdaHandler {
func handle(context: Lambda.Context, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer?>) {
switch self.codec.decode(payload) {
case .failure(let error):
return callback(.failure(Errors.requestDecoding(error)))
return promise.fail(Errors.requestDecoding(error))
case .success(let payloadAsCodable):
self.handle(context: context, payload: payloadAsCodable) { result in
switch result {
case .none:
promise.succeed(nil)
case .failure(let error):
return callback(.failure(error))
promise.fail(error)
case .success(let encodable):
switch self.codec.encode(encodable) {
case .failure(let error):
return callback(.failure(Errors.responseEncoding(error)))
case .success(let codableAsBytes):
return callback(.success(codableAsBytes))
promise.fail(Errors.responseEncoding(error))
case .success(let buffer):
promise.succeed(buffer)
}
}
}
}
}
}

/// LambdaCodableJsonCodec is an implementation of `LambdaCodableCodec` which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
/// Default implementation of `Encodable` -> `ByteBuffer` encoding and `ByteBuffer` -> `Decodable'`decoding
public extension CodablePromiseLambdaHandler {
func handle(context: Lambda.Context, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer?>) {
switch self.codec.decode(payload) {
case .failure(let error):
return promise.fail(Errors.requestDecoding(error))
case .success(let decodable):
let encodablePromise = context.eventLoop.makePromise(of: Out?.self)
encodablePromise.futureResult.flatMapThrowing { encodable in
try encodable.flatMap { encodable in
switch self.codec.encode(encodable) {
case .failure(let error):
throw Errors.responseEncoding(error)
case .success(let buffer):
return buffer
}
}
}.cascade(to: promise)
self.handle(context: context, payload: decodable, promise: encodablePromise)
}
}
}

/// LambdaCodableJsonCodec is an implementation of `LambdaCodableCodec` which does `Encodable` -> `ByteBuffer` encoding and `ByteBuffer` -> `Decodable' decoding
/// using JSONEncoder and JSONDecoder respectively.
// This is a class as encoder amd decoder are a class, which means its cheaper to hold a reference to both in a class then a struct.
private final class LambdaCodableJsonCodec<In: Decodable, Out: Encodable>: LambdaCodableCodec<In, Out> {
Copy link
Member

Choose a reason for hiding this comment

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

Do we maybe want to move the default Codable implementation into another package? This way we could not use Foundation at all in the main package. We could use the same naming as NIOFoundationCompat so for example AWSLambdaRuntimeFoundationCompat. We could even use @_exported within this package so that "normal" users only have to import this one package. This would allow developers, like myself, who want to be as barebones as possible, to not link Foundation at all.

Copy link
Member

Choose a reason for hiding this comment

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

Btw. in the AWSLambdaRuntimeFoundationCompat package we could also offer callbacks based on Foundation.Data, so we don't have to use [UInt8] in there, something that looks quite strange to the average iOS developer.

private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let allocator = ByteBufferAllocator()

public override func encode(_ value: Out) -> Result<[UInt8], Error> {
public override func encode(_ value: Out) -> Result<ByteBuffer, Error> {
do {
return .success(try [UInt8](self.encoder.encode(value)))
let data = try self.encoder.encode(value)
var buffer = self.allocator.buffer(capacity: data.count)
buffer.writeBytes(data)
return .success(buffer)
} catch {
return .failure(error)
}
}

public override func decode(_ data: [UInt8]) -> Result<In, Error> {
public override func decode(_ buffer: ByteBuffer) -> Result<In, Error> {
do {
return .success(try self.decoder.decode(In.self, from: Data(data)))
guard let data = buffer.getData(at: buffer.readerIndex, length: buffer.readableBytes) else {
throw Errors.invalidBuffer
}
return .success(try self.decoder.decode(In.self, from: data))
} catch {
return .failure(error)
}
}
}

private struct LambdaClosureWrapper<In: Decodable, Out: Encodable>: LambdaCodableHandler {
private struct ClosureWrapper<In: Decodable, Out: Encodable>: CodableLambdaHandler {
typealias Codec = LambdaCodableJsonCodec<In, Out>

private let closure: LambdaCodableClosure<In, Out>
init(_ closure: @escaping LambdaCodableClosure<In, Out>) {
private let closure: CodableLambda.Closure<In, Out>

init(_ closure: @escaping CodableLambda.Closure<In, Out>) {
self.closure = closure
}

public func handle(context: LambdaContext, payload: In, callback: @escaping LambdaCodableCallback<Out>) {
public func handle(context: Lambda.Context, payload: In, callback: @escaping CodableLambda.CompletionHandler<Out>) {
self.closure(context, payload, callback)
}
}

private enum Errors: Error {
case responseEncoding(Error)
case requestDecoding(Error)
case invalidBuffer
}
Loading