-
Notifications
You must be signed in to change notification settings - Fork 118
[DNM][RFC] API-2 #21
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
[DNM][RFC] API-2 #21
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>) | ||
} | ||
|
||
/// 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?>) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if we want to go with an |
||
} | ||
|
||
/// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, if we should stay with the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Btw. in the |
||
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 | ||
} |
There was a problem hiding this comment.
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:
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 onCodablePromiseLambdaHandler
? 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.