Skip to content

APIGateway.Request Version 1 Support #1

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

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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
82 changes: 82 additions & 0 deletions Sources/MacroLambdaCore/LambdaRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,61 @@ public extension IncomingMessage {
lambdaGatewayRequest = lambdaRequest
}

convenience init(lambdaRequest : APIGateway.Request,
log : Logger = .init(label: "μ.http"))
{
// version doesn't matter, we don't really do HTTP
var head = HTTPRequestHead(
version : .init(major: 1, minor: 1),
method : lambdaRequest.httpMethod.asNIO,
uri : lambdaRequest.path
)
head.headers = lambdaRequest.headers.asNIO

/* APIGateway.Request V1 (REST) do not support cookies.

if let cookies = lambdaRequest.cookies, !cookies.isEmpty {
// So our "connect" module expects them in the headers, so we'd need
// to serialize them again ...
// The `IncomingMessage` also has a `cookies` getter, but I think that
// isn't cached.
for cookie in cookies { // that is weird too, is it right?
head.headers.add(name: "Cookie", value: cookie)
}
}
*/

// TBD: there is also "pathParameters", what is that, URL fragments (#)?
if let pathParams = lambdaRequest.pathParameters, !pathParams.isEmpty {
log.warning("ignoring lambda path parameters: \(pathParams)")
}

if let qsParameters = lambdaRequest.queryStringParameters,
!qsParameters.isEmpty
{
// TBD: is that included in the path?
var isFirst = false
if !head.uri.contains("?") { head.uri.append("?"); isFirst = true }
for ( key, value ) in qsParameters {
if isFirst { isFirst = false }
else { head.uri += "&" }

head.uri +=
key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
?? key
head.uri += "="
head.uri +=
value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
?? value
}
}

self.init(head, socket: nil, log: log)

// and keep the whole thing
lambdaV1GatewayRequest = lambdaRequest
}

internal func sendLambdaBody(_ lambdaRequest: APIGateway.V2.Request) {
defer { push(nil) }

Expand All @@ -85,6 +140,23 @@ public extension IncomingMessage {
emit(error: error)
}
}

internal func sendLambdaBody(_ lambdaRequest: APIGateway.Request) {
defer { push(nil) }

guard let body = lambdaRequest.body else { return }
do {
if lambdaRequest.isBase64Encoded {
push(try Buffer.from(body, "base64"))
}
else {
push(try Buffer.from(body))
}
}
catch {
emit(error: error)
}
}
}


Expand All @@ -93,11 +165,21 @@ enum LambdaRequestKey: EnvironmentKey {
static let loggingKey = "lambda-request"
}

enum LambdaV1RequestKey: EnvironmentKey {
static let defaultValue : APIGateway.Request? = nil
static let loggingKey = "lambda-request"
}

public extension IncomingMessage {

var lambdaGatewayRequest: APIGateway.V2.Request? {
set { environment[LambdaRequestKey.self] = newValue }
get { return environment[LambdaRequestKey.self] }
}

var lambdaV1GatewayRequest: APIGateway.Request? {
set { environment[LambdaV1RequestKey.self] = newValue }
get { return environment[LambdaV1RequestKey.self] }
}
}
#endif // canImport(AWSLambdaEvents)
29 changes: 29 additions & 0 deletions Sources/MacroLambdaCore/LambdaResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ extension ServerResponse {
isBase64Encoded : body != nil ? true : false,
cookies : cookies)
}

var asLambdaV1GatewayResponse: APIGateway.Response {
assert(writableEnded, "sending ServerResponse which didn't end?!")

let ( singleHeaders, multiHeaders, _ ) = headers.asLambda()

let body : String? = {
guard let writtenContent = writableBuffer, !writtenContent.isEmpty else {
return nil
}

// TBD: We could make this more tolerant and use a String if the content
// is textual and can be converted to UTF-8? Would make it faster as
// well.
do {
return try writtenContent.toString("base64")
}
catch { // FIXME: make throwing
log.error("could not convert body to base64: \(error)")
return nil
}
}()

return .init(statusCode : status.asLambda,
headers : singleHeaders,
multiValueHeaders : multiHeaders,
body : body,
isBase64Encoded : body != nil ? true : false)
}
}

#endif // canImport(AWSLambdaEvents)
80 changes: 80 additions & 0 deletions Sources/MacroLambdaCore/LambdaServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,25 @@ extension lambda {
return promise.futureResult
}
}

struct APIGatewayV1ProxyLambda: EventLoopLambdaHandler {
typealias In = APIGateway.Request
typealias Out = APIGateway.Response

let server : Server

func handle(context: Lambda.Context, event: In) -> EventLoopFuture<Out>
{
let promise = context.eventLoop.makePromise(of: Out.self)
server.handle(context: context, request: event) { result in
promise.completeWith(result)
}
return promise.futureResult
}
}

// FIXME: This proxy is where the Lambda Request payload type is determined for Codable decoding.
// Need to figure out how to select or determine type.
let proxy = APIGatewayProxyLambda(server: self)
Lambda.run(proxy)
Foundation.exit(0) // Because `run` is not marked as Never (Issue #151)
Expand Down Expand Up @@ -257,6 +276,67 @@ extension lambda {
assert(didFinish)
}
}

private func handle(context : Lambda.Context,
request : APIGateway.Request,
callback : @escaping
( Result<APIGateway.Response, Error> ) -> Void)
{
guard !self._requestListeners.isEmpty else {
assertionFailure("no request listeners?!")
return callback(.failure(ServerError.noRequestListeners))
}

let req = IncomingMessage(lambdaRequest: request, log: context.logger)
let res = ServerResponse(unsafeChannel: nil, log: context.logger)
res.cork()
res.request = req

// The transaction ends when the response is done, not when the
// request was read completely!
var didFinish = false

res.onceFinish {
// convert res to gateway Response and call callback
guard !didFinish else {
return context.logger.error("TX already finished!")
}
didFinish = true

callback(.success(res.asLambdaV1GatewayResponse))
}

res.onError { error in
guard !didFinish else {
return context.logger.error("Follow up error: \(error)")
}
didFinish = true
callback(.failure(error))
}

// TODO: Process Expect. It's not really "ahead of sending the body",
// but we still need to validate the preconditions. http.Server
// has code for this. Do the same.

do { // onRequest
var listeners = self._requestListeners // Note: No `once` support!
guard !listeners.isEmpty else {
didFinish = true
return callback(.failure(ServerError.noRequestListeners))
}

listeners.emit(( req, res ))
}

// For a streaming push, we do the lambda-send here, after announcing the
// head.
if !res.writableEnded { // response is already closed
req.sendLambdaBody(request)
}
else {
assert(didFinish)
}
}
}

enum ServerError: Swift.Error {
Expand Down