diff --git a/Sources/MacroLambdaCore/LambdaRequest.swift b/Sources/MacroLambdaCore/LambdaRequest.swift index 489b89b..2e5cad2 100644 --- a/Sources/MacroLambdaCore/LambdaRequest.swift +++ b/Sources/MacroLambdaCore/LambdaRequest.swift @@ -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) } @@ -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) + } + } } @@ -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) diff --git a/Sources/MacroLambdaCore/LambdaResponse.swift b/Sources/MacroLambdaCore/LambdaResponse.swift index 72530ea..af6e5ed 100644 --- a/Sources/MacroLambdaCore/LambdaResponse.swift +++ b/Sources/MacroLambdaCore/LambdaResponse.swift @@ -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) diff --git a/Sources/MacroLambdaCore/LambdaServer.swift b/Sources/MacroLambdaCore/LambdaServer.swift index bc6955c..53b4d9a 100644 --- a/Sources/MacroLambdaCore/LambdaServer.swift +++ b/Sources/MacroLambdaCore/LambdaServer.swift @@ -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 + { + 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) @@ -257,6 +276,67 @@ extension lambda { assert(didFinish) } } + + private func handle(context : Lambda.Context, + request : APIGateway.Request, + callback : @escaping + ( Result ) -> 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 {