Skip to content

Commit 7d1b309

Browse files
authored
refactor api 1 (#33)
motivation: nicer apis, happier users changes: * use ByteBuffer + EventLoopFuture for core APIs instead of byte array and callbacks * create three base protocols: ByteBufferLambdaHandler, EventLoopLambdaHandler and LambdaHandler * abstract common encoding/decoding functionality into a EventLoopLambdaHandler, reducing most code from string/codable handlers * only the highest level LambdaHandler is offloaded to a DispatchQueue, lower level is run on the same EventLoop as the core library * refine encoding/decoding logic * inline all the things * adjust tests * adjust api docs * adjust readme
1 parent 9d15930 commit 7d1b309

25 files changed

+922
-582
lines changed

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ let package = Package(
1313
.package(url: "https://github.com/swift-server/swift-backtrace.git", from: "1.1.0"),
1414
],
1515
targets: [
16-
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1"]),
16+
.target(name: "SwiftAwsLambda", dependencies: ["Logging", "Backtrace", "NIOHTTP1", "NIOFoundationCompat"]),
1717
.testTarget(name: "SwiftAwsLambdaTests", dependencies: ["SwiftAwsLambda"]),
1818
// samples
19-
.target(name: "SwiftAwsLambdaSample", dependencies: ["SwiftAwsLambda"]),
2019
.target(name: "SwiftAwsLambdaStringSample", dependencies: ["SwiftAwsLambda"]),
2120
.target(name: "SwiftAwsLambdaCodableSample", dependencies: ["SwiftAwsLambda"]),
2221
// perf tests

Sources/MockServer/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ internal final class HTTPHandler: ChannelInboundHandler {
148148
if case .failure(let error) = result {
149149
self.logger.error("\(self) write error \(error)")
150150
}
151-
if !self.self.keepAlive {
151+
if !self.keepAlive {
152152
context.close().whenFailure { error in
153153
self.logger.error("\(self) close error \(error)")
154154
}

Sources/SwiftAwsLambda/HttpClient.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal final class HTTPClient {
4040
timeout: timeout ?? self.configuration.requestTimeout))
4141
}
4242

43-
func post(url: String, body: ByteBuffer, timeout: TimeAmount? = nil) -> EventLoopFuture<Response> {
43+
func post(url: String, body: ByteBuffer?, timeout: TimeAmount? = nil) -> EventLoopFuture<Response> {
4444
return self.execute(Request(targetHost: self.targetHost,
4545
url: url,
4646
method: .POST,
@@ -145,12 +145,14 @@ private final class HTTPHandler: ChannelDuplexHandler {
145145
let request = unwrapOutboundIn(data)
146146

147147
var head = HTTPRequestHead(version: .init(major: 1, minor: 1), method: request.method, uri: request.url, headers: request.headers)
148-
if !head.headers.contains(name: "Host") {
149-
head.headers.add(name: "Host", value: request.targetHost)
150-
}
151-
if let body = request.body {
152-
head.headers.add(name: "Content-Length", value: String(body.readableBytes))
148+
head.headers.add(name: "host", value: request.targetHost)
149+
switch request.method {
150+
case .POST, .PUT:
151+
head.headers.add(name: "content-length", value: String(request.body?.readableBytes ?? 0))
152+
default:
153+
break
153154
}
155+
154156
// We don't add a "Connection" header here if we want to keep the connection open,
155157
// HTTP/1.1 defines specifies the following in RFC 2616, Section 8.1.2.1:
156158
//
@@ -163,7 +165,7 @@ private final class HTTPHandler: ChannelDuplexHandler {
163165
//
164166
// See also UnaryHandler.channelRead below.
165167
if !self.keepAlive {
166-
head.headers.add(name: "Connection", value: "close")
168+
head.headers.add(name: "connection", value: "close")
167169
}
168170

169171
context.write(self.wrapOutboundOut(HTTPClientRequestPart.head(head))).flatMap { _ -> EventLoopFuture<Void> in

Sources/SwiftAwsLambda/Lambda+Codable.swift

Lines changed: 82 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -12,118 +12,115 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation // for JSON
15+
import class Foundation.JSONDecoder
16+
import class Foundation.JSONEncoder
17+
import NIO
18+
import NIOFoundationCompat
1619

1720
/// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` payloads.
18-
/// This is the most common way to use this library in AWS Lambda, since its JSON based.
1921
extension Lambda {
20-
/// Run a Lambda defined by implementing the `LambdaCodableClosure` closure, having `In` and `Out` extending `Decodable` and `Encodable` respectively.
22+
/// Run a Lambda defined by implementing the `CodableLambdaClosure` function.
2123
///
22-
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
23-
public static func run<In: Decodable, Out: Encodable>(_ closure: @escaping LambdaCodableClosure<In, Out>) {
24-
self.run(LambdaClosureWrapper(closure))
24+
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
25+
@inlinable
26+
public static func run<In: Decodable, Out: Encodable>(_ closure: @escaping CodableLambdaClosure<In, Out>) {
27+
self.run(closure: closure)
28+
}
29+
30+
/// Run a Lambda defined by implementing the `CodableVoidLambdaClosure` function.
31+
///
32+
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
33+
@inlinable
34+
public static func run<In: Decodable>(_ closure: @escaping CodableVoidLambdaClosure<In>) {
35+
self.run(closure: closure)
36+
}
37+
38+
// for testing
39+
@inlinable
40+
@discardableResult
41+
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping CodableLambdaClosure<In, Out>) -> Result<Int, Error> {
42+
return self.run(configuration: configuration, handler: CodableLambdaClosureWrapper(closure))
2543
}
2644

2745
// for testing
28-
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
29-
return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure))
46+
@inlinable
47+
@discardableResult
48+
internal static func run<In: Decodable>(configuration: Configuration = .init(), closure: @escaping CodableVoidLambdaClosure<In>) -> Result<Int, Error> {
49+
return self.run(configuration: configuration, handler: CodableVoidLambdaClosureWrapper(closure))
3050
}
3151
}
3252

33-
/// A callback for a Lambda that returns a `Result<Out, Error>` result type, having `Out` extend `Encodable`.
34-
public typealias LambdaCodableCallback<Out> = (Result<Out, Error>) -> Void
53+
/// A processing closure for a Lambda that takes a `In` and returns a `Result<Out, Error>` via a `CompletionHandler` asynchronously.
54+
public typealias CodableLambdaClosure<In: Decodable, Out: Encodable> = (Lambda.Context, In, @escaping (Result<Out, Error>) -> Void) -> Void
3555

36-
/// A processing closure for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
37-
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
38-
public typealias LambdaCodableClosure<In, Out> = (Lambda.Context, In, LambdaCodableCallback<Out>) -> Void
56+
/// A processing closure for a Lambda that takes a `In` and returns a `Result<Void, Error>` via a `CompletionHandler` asynchronously.
57+
public typealias CodableVoidLambdaClosure<In: Decodable> = (Lambda.Context, In, @escaping (Result<Void, Error>) -> Void) -> Void
3958

40-
/// A processing protocol for a Lambda that takes an `In` and returns an `Out` via `LambdaCodableCallback<Out>` asynchronously,
41-
/// having `In` and `Out` extending `Decodable` and `Encodable` respectively.
42-
public protocol LambdaCodableHandler: LambdaHandler {
43-
associatedtype In: Decodable
44-
associatedtype Out: Encodable
59+
@usableFromInline
60+
internal struct CodableLambdaClosureWrapper<In: Decodable, Out: Encodable>: LambdaHandler {
61+
@usableFromInline
62+
typealias In = In
63+
@usableFromInline
64+
typealias Out = Out
4565

46-
func handle(context: Lambda.Context, payload: In, callback: @escaping LambdaCodableCallback<Out>)
47-
var codec: LambdaCodableCodec<In, Out> { get }
48-
}
66+
private let closure: CodableLambdaClosure<In, Out>
4967

50-
/// Default implementation for `LambdaCodableHandler` codec which uses JSON via `LambdaCodableJsonCodec`.
51-
/// Advanced users that want to inject their own codec can do it by overriding this.
52-
public extension LambdaCodableHandler {
53-
var codec: LambdaCodableCodec<In, Out> {
54-
return LambdaCodableJsonCodec<In, Out>()
68+
@usableFromInline
69+
init(_ closure: @escaping CodableLambdaClosure<In, Out>) {
70+
self.closure = closure
5571
}
56-
}
57-
58-
/// LambdaCodableCodec is an abstract/empty implementation for codec which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding.
59-
// TODO: would be nicer to use a protocol instead of this "abstract class", but generics get in the way
60-
public class LambdaCodableCodec<In: Decodable, Out: Encodable> {
61-
func encode(_: Out) -> Result<[UInt8], Error> { fatalError("not implmented") }
62-
func decode(_: [UInt8]) -> Result<In, Error> { fatalError("not implmented") }
63-
}
6472

65-
/// Default implementation of `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
66-
public extension LambdaCodableHandler {
67-
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) {
68-
switch self.codec.decode(payload) {
69-
case .failure(let error):
70-
return callback(.failure(Errors.requestDecoding(error)))
71-
case .success(let payloadAsCodable):
72-
self.handle(context: context, payload: payloadAsCodable) { result in
73-
switch result {
74-
case .failure(let error):
75-
return callback(.failure(error))
76-
case .success(let encodable):
77-
switch self.codec.encode(encodable) {
78-
case .failure(let error):
79-
return callback(.failure(Errors.responseEncoding(error)))
80-
case .success(let codableAsBytes):
81-
return callback(.success(codableAsBytes))
82-
}
83-
}
84-
}
85-
}
73+
@usableFromInline
74+
func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void) {
75+
self.closure(context, payload, callback)
8676
}
8777
}
8878

89-
/// LambdaCodableJsonCodec is an implementation of `LambdaCodableCodec` which does `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
90-
/// using JSONEncoder and JSONDecoder respectively.
91-
// 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.
92-
private final class LambdaCodableJsonCodec<In: Decodable, Out: Encodable>: LambdaCodableCodec<In, Out> {
93-
private let encoder = JSONEncoder()
94-
private let decoder = JSONDecoder()
95-
96-
public override func encode(_ value: Out) -> Result<[UInt8], Error> {
97-
do {
98-
return .success(try [UInt8](self.encoder.encode(value)))
99-
} catch {
100-
return .failure(error)
101-
}
79+
@usableFromInline
80+
internal struct CodableVoidLambdaClosureWrapper<In: Decodable>: LambdaHandler {
81+
@usableFromInline
82+
typealias In = In
83+
@usableFromInline
84+
typealias Out = Void
85+
86+
private let closure: CodableVoidLambdaClosure<In>
87+
88+
@usableFromInline
89+
init(_ closure: @escaping CodableVoidLambdaClosure<In>) {
90+
self.closure = closure
10291
}
10392

104-
public override func decode(_ data: [UInt8]) -> Result<In, Error> {
105-
do {
106-
return .success(try self.decoder.decode(In.self, from: Data(data)))
107-
} catch {
108-
return .failure(error)
109-
}
93+
@usableFromInline
94+
func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void) {
95+
self.closure(context, payload, callback)
11096
}
11197
}
11298

113-
private struct LambdaClosureWrapper<In: Decodable, Out: Encodable>: LambdaCodableHandler {
114-
typealias Codec = LambdaCodableJsonCodec<In, Out>
115-
116-
private let closure: LambdaCodableClosure<In, Out>
117-
init(_ closure: @escaping LambdaCodableClosure<In, Out>) {
118-
self.closure = closure
99+
/// Implementation of a`ByteBuffer` to `In` and `Out` to `ByteBuffer` codec
100+
/// Using Foundation's JSONEncoder and JSONDecoder
101+
/// Advanced users that want to inject their own codec can do it by overriding these functions.
102+
public extension EventLoopLambdaHandler where In: Decodable, Out: Encodable {
103+
func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? {
104+
// nio will resize the buffer if necessary
105+
// FIXME: reusable JSONEncoder and buffer
106+
var buffer = allocator.buffer(capacity: 1024)
107+
try JSONEncoder().encode(value, into: &buffer)
108+
return buffer
119109
}
120110

121-
public func handle(context: Lambda.Context, payload: In, callback: @escaping LambdaCodableCallback<Out>) {
122-
self.closure(context, payload, callback)
111+
func decode(buffer: ByteBuffer) throws -> In {
112+
// FIXME: reusable JSONDecoder
113+
return try JSONDecoder().decode(In.self, from: buffer)
123114
}
124115
}
125116

126-
private enum Errors: Error {
127-
case responseEncoding(Error)
128-
case requestDecoding(Error)
117+
public extension EventLoopLambdaHandler where In: Decodable, Out == Void {
118+
func encode(allocator: ByteBufferAllocator, value: Void) throws -> ByteBuffer? {
119+
return nil
120+
}
121+
122+
func decode(buffer: ByteBuffer) throws -> In {
123+
// FIXME: reusable JSONDecoder
124+
return try JSONDecoder().decode(In.self, from: buffer)
125+
}
129126
}

0 commit comments

Comments
 (0)