-
Notifications
You must be signed in to change notification settings - Fork 118
[DNM][RFC] alternative api #18
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
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,6 +13,8 @@ | |
//===----------------------------------------------------------------------===// | ||
|
||
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. | ||
|
@@ -32,21 +34,18 @@ extension Lambda { | |
} | ||
|
||
// for testing | ||
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult { | ||
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> Result<Int, Error> { | ||
return self.run(handler: LambdaClosureWrapper(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: LambdaCodableHandler { | ||
return self.run(handler: handler 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> | ||
|
||
/// A callback for a Lambda that returns a `LambdaCodableResult<Out>` result type, having `Out` extend `Encodable`. | ||
public typealias LambdaCodableCallback<Out> = (LambdaCodableResult<Out>) -> Void | ||
/// A callback for a Lambda that returns a `Result<Out, Error>` result type, having `Out` extend `Encodable`. | ||
public typealias LambdaCodableCallback<Out> = (Result<Out, Error>) -> Void | ||
|
||
/// A processing closure for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously, | ||
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively. | ||
|
@@ -73,27 +72,27 @@ public extension LambdaCodableHandler { | |
/// LambdaCodableCodec is an abstract/empty implementation for codec which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `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") } | ||
} | ||
|
||
/// Default implementation of `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding | ||
public extension LambdaCodableHandler { | ||
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping (LambdaResult) -> Void) { | ||
func handle(context: LambdaContext, 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 | ||
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. For every non EventLoopPromise based callback I'd build an offloaded sync/async version. |
||
switch result { | ||
case .failure(let error): | ||
return callback(.failure(error)) | ||
return 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)) | ||
return promise.fail(Errors.responseEncoding(error)) | ||
case .success(let buffer): | ||
return promise.succeed(buffer) | ||
} | ||
} | ||
} | ||
|
@@ -107,18 +106,25 @@ public extension LambdaCodableHandler { | |
private final class LambdaCodableJsonCodec<In: Decodable, Out: Encodable>: LambdaCodableCodec<In, Out> { | ||
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)) | ||
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. You return .success(try self.decoder.decode(In.self, from: buffer)) 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. iirc that returns an optional, so we would end up with same # loc 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 afraid if something is invalid it throws, otherwise returns a nonoptional. |
||
} catch { | ||
return .failure(error) | ||
} | ||
|
@@ -141,4 +147,5 @@ private struct LambdaClosureWrapper<In: Decodable, Out: Encodable>: LambdaCodabl | |
private enum Errors: Error { | ||
case responseEncoding(Error) | ||
case requestDecoding(Error) | ||
case invalidBuffer | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ | |
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import NIO | ||
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 remove this file for now? When playing around with the handler syntax, this is just a shore to fix. Could be added back later, without any problems. |
||
|
||
/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `String` payloads. | ||
extension Lambda { | ||
/// Run a Lambda defined by implementing the `LambdaStringClosure` protocol. | ||
|
@@ -29,21 +31,18 @@ extension Lambda { | |
} | ||
|
||
// for testing | ||
internal static func run(configuration: Configuration = .init(), _ closure: @escaping LambdaStringClosure) -> LambdaLifecycleResult { | ||
internal static func run(configuration: Configuration = .init(), _ closure: @escaping LambdaStringClosure) -> Result<Int, Error> { | ||
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration) | ||
} | ||
|
||
// for testing | ||
internal static func run(handler: LambdaStringHandler, configuration: Configuration = .init()) -> LambdaLifecycleResult { | ||
internal static func run(handler: LambdaStringHandler, configuration: Configuration = .init()) -> Result<Int, Error> { | ||
return self.run(handler: handler as LambdaHandler, configuration: configuration) | ||
} | ||
} | ||
|
||
/// A result type for a Lambda that returns a `String`. | ||
public typealias LambdaStringResult = Result<String, Error> | ||
|
||
/// A callback for a Lambda that returns a `LambdaStringResult` result type. | ||
public typealias LambdaStringCallback = (LambdaStringResult) -> Void | ||
/// A callback for a Lambda that returns a `Result<String, Error>` result type. | ||
public typealias LambdaStringCallback = (Result<String, Error>) -> Void | ||
|
||
/// A processing closure for a Lambda that takes a `String` and returns a `LambdaStringResult` via `LambdaStringCallback` asynchronously. | ||
public typealias LambdaStringClosure = (LambdaContext, String, LambdaStringCallback) -> Void | ||
|
@@ -55,13 +54,18 @@ public protocol LambdaStringHandler: LambdaHandler { | |
|
||
/// Default implementation of `String` -> `[UInt8]` encoding and `[UInt8]` -> `String' decoding | ||
public extension LambdaStringHandler { | ||
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) { | ||
self.handle(context: context, payload: String(decoding: payload, as: UTF8.self)) { result in | ||
func handle(context: LambdaContext, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer>) { | ||
guard let payload = payload.getString(at: payload.readerIndex, length: payload.readableBytes) else { | ||
return promise.fail(Errors.invalidBuffer) | ||
} | ||
self.handle(context: context, payload: payload) { result in | ||
switch result { | ||
case .success(let string): | ||
return callback(.success([UInt8](string.utf8))) | ||
var buffer = context.allocator.buffer(capacity: string.utf8.count) | ||
buffer.writeString(string) | ||
return promise.succeed(buffer) | ||
case .failure(let error): | ||
return callback(.failure(error)) | ||
return promise.fail(error) | ||
} | ||
} | ||
} | ||
|
@@ -77,3 +81,7 @@ private struct LambdaClosureWrapper: LambdaStringHandler { | |
self.closure(context, payload, callback) | ||
} | ||
} | ||
|
||
private enum Errors: Error { | ||
case invalidBuffer | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,14 +44,14 @@ public enum Lambda { | |
// for testing and internal use | ||
@usableFromInline | ||
@discardableResult | ||
internal static func run(configuration: Configuration = .init(), closure: @escaping LambdaClosure) -> LambdaLifecycleResult { | ||
internal static func run(configuration: Configuration = .init(), closure: @escaping LambdaClosure) -> Result<Int, Error> { | ||
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration) | ||
} | ||
|
||
// for testing and internal use | ||
@usableFromInline | ||
@discardableResult | ||
internal static func run(handler: LambdaHandler, configuration: Configuration = .init()) -> LambdaLifecycleResult { | ||
internal static func run(handler: LambdaHandler, configuration: Configuration = .init()) -> Result<Int, Error> { | ||
do { | ||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) // only need one thread, will improve performance | ||
defer { try! eventLoopGroup.syncShutdownGracefully() } | ||
|
@@ -252,32 +252,20 @@ public enum Lambda { | |
} | ||
} | ||
|
||
/// A result type for a Lambda that returns a `[UInt8]`. | ||
public typealias LambdaResult = Result<[UInt8], Error> | ||
|
||
public typealias LambdaCallback = (LambdaResult) -> Void | ||
/// A callback for a Lambda that returns a `Result<[UInt8], Error>`. | ||
public typealias LambdaCallback = (Result<[UInt8], Error>) -> Void | ||
|
||
/// A processing closure for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously. | ||
public typealias LambdaClosure = (LambdaContext, [UInt8], LambdaCallback) -> Void | ||
|
||
/// A result type for a Lambda initialization. | ||
public typealias LambdaInitResult = Result<Void, Error> | ||
|
||
/// A callback to provide the result of Lambda initialization. | ||
public typealias LambdaInitCallBack = (LambdaInitResult) -> Void | ||
|
||
/// A processing protocol for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously. | ||
public protocol LambdaHandler { | ||
/// Initializes the `LambdaHandler`. | ||
func initialize(callback: @escaping LambdaInitCallBack) | ||
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) | ||
func handle(context: LambdaContext, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer>) | ||
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. As stated in #11 (comment) the returning 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. +1 |
||
} | ||
|
||
extension LambdaHandler { | ||
@inlinable | ||
public func initialize(callback: @escaping LambdaInitCallBack) { | ||
callback(.success(())) | ||
} | ||
public protocol InitializableLambdaHandler { | ||
/// Initializes the `LambdaHandler`. | ||
func initialize(promise: EventLoopPromise<Void>) | ||
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 can totally see the idea of using the I'm not sure though if it would be better to go all in and make the For example we could set it up like this public protocol InitializableLambdaHandler {
@inlinable /// Initializes the `LambdaHandler`.
func init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) throws
} I can see that this looks kind of ugly injecting the
struct MyLambdaHandler: InitializableLambdaHandler, LambdaHandler {
let httpClient: AsyncHTTPClient
init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) throws {
self.httpClient = AsyncHTTPClient(.shared(eventLoopGroup))
// other setup with eventLoopGroup: database clients, aws sdk, whatevar.
// in context of aws especially important is getting keys from secret store,
// which is why the promise might be important
promise.succeed(Void())
}
func handle() {
...
}
} Getting access to 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. on one hand I like the idea of I also dont think we need to make it a throwable. if you want to return an error catch it and fail the promise 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 don't know if it would compile though. ;) struct MyLambdaHandler: InitializableLambdaHandler, LambdaHandler {
let httpClient: AsyncHTTPClient
init(promise: EventLoopPromise<Void>, eventLoopGroup: EventLoopGroup) {
if somethingUnexpected {
return // not everything is initialized here
}
self.httpClient = AsyncHTTPClient()
}
If we want to report an error during bootup, we could also offer a static function. Then we don't have to do the init dance. Lambda.reportBootupError(_: error) 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. another alternative is to define an optional blocking initializer and that's it, so users won't be able to to any significant work in it but the signature can be simplified to:
but this closes the window on doing things like getting one-time-initialization-type-data from some database or upstream service, which could be handy or another option is to have both: an both could/should be optional. maybe that's the best? 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. We should insert the complete I don't know if we can make an |
||
} | ||
|
||
public struct LambdaContext { | ||
|
@@ -289,6 +277,8 @@ public struct LambdaContext { | |
public let clientContext: String? | ||
public let deadline: String? | ||
// utliity | ||
public let eventLoop: EventLoop | ||
public let allocator: ByteBufferAllocator | ||
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. +1 for having the |
||
public let logger: Logger | ||
|
||
public init(requestId: String, | ||
|
@@ -297,13 +287,17 @@ public struct LambdaContext { | |
cognitoIdentity: String? = nil, | ||
clientContext: String? = nil, | ||
deadline: String? = nil, | ||
eventLoop: EventLoop, | ||
logger: Logger) { | ||
self.requestId = requestId | ||
self.traceId = traceId | ||
self.invokedFunctionArn = invokedFunctionArn | ||
self.cognitoIdentity = cognitoIdentity | ||
self.clientContext = clientContext | ||
self.deadline = deadline | ||
// utility | ||
self.eventLoop = eventLoop | ||
self.allocator = ByteBufferAllocator() | ||
// mutate logger with context | ||
var logger = logger | ||
logger[metadataKey: "awsRequestId"] = .string(requestId) | ||
|
@@ -314,9 +308,6 @@ public struct LambdaContext { | |
} | ||
} | ||
|
||
@usableFromInline | ||
internal typealias LambdaLifecycleResult = Result<Int, Error> | ||
|
||
private struct LambdaClosureWrapper: LambdaHandler { | ||
private let closure: LambdaClosure | ||
init(_ closure: @escaping LambdaClosure) { | ||
|
@@ -326,4 +317,17 @@ private struct LambdaClosureWrapper: LambdaHandler { | |
func handle(context: LambdaContext, payload: [UInt8], callback: @escaping LambdaCallback) { | ||
self.closure(context, payload, callback) | ||
} | ||
|
||
func handle(context: LambdaContext, payload: ByteBuffer, promise: EventLoopPromise<ByteBuffer>) { | ||
self.closure(context, payload.getBytes(at: payload.readerIndex, length: payload.readableBytes) ?? []) { result in | ||
switch result { | ||
case .success(let bytes): | ||
var buffer = context.allocator.buffer(capacity: bytes.count) | ||
buffer.writeBytes(bytes) | ||
promise.succeed(buffer) | ||
case .failure(let error): | ||
promise.fail(error) | ||
} | ||
} | ||
} | ||
} |
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.
Just out of curiosity why do we use
Result
here and not plain oldthrows
?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.
mostly my personal dislike of try/catch blocks