Skip to content
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

Lambda runtime v2 #42

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
timeout-minutes: 15
strategy:
matrix:
image: ["swift:5.9", "swift:5.10", "swift:6.0"]
image: ["swift:6.0"]
container:
image: ${{ matrix.image }}
steps:
Expand Down
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

import PackageDescription

let package = Package(
name: "hummingbird-lambda",
platforms: [
.macOS(.v14)
.macOS(.v15)
],
products: [
.library(name: "HummingbirdLambda", targets: ["HummingbirdLambda"]),
.library(name: "HummingbirdLambdaTesting", targets: ["HummingbirdLambdaTesting"]),
.executable(name: "HBLambdaTest", targets: ["HBLambdaTest"]),
],
dependencies: [
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.2"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "cancel-next-invocation"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.4.0"),
.package(url: "https://github.com/swift-extras/swift-extras-base64.git", from: "1.0.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
Expand Down
21 changes: 10 additions & 11 deletions Sources/HBLambdaTest/maths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import Hummingbird
import HummingbirdLambda
import Logging

typealias AppRequestContext = BasicLambdaRequestContext<APIGatewayV2Request>

struct DebugMiddleware: RouterMiddleware {
typealias Context = MathsHandler.Context
typealias Context = AppRequestContext
func handle(
_ request: Request,
context: Context,
Expand All @@ -33,9 +35,7 @@ struct DebugMiddleware: RouterMiddleware {
}

@main
struct MathsHandler: APIGatewayLambdaFunction {
typealias Context = BasicLambdaRequestContext<APIGatewayRequest>

struct MathsLambda {
struct Operands: Decodable {
let lhs: Double
let rhs: Double
Expand All @@ -45,10 +45,8 @@ struct MathsHandler: APIGatewayLambdaFunction {
let result: Double
}

init(context: LambdaInitializationContext) {}

func buildResponder() -> some HTTPResponder<Context> {
let router = Router(context: Context.self)
static func main() async throws {
let router = Router(context: AppRequestContext.self)
router.middlewares.add(DebugMiddleware())
router.post("add") { request, context -> Result in
let operands = try await request.decode(as: Operands.self, context: context)
Expand All @@ -66,8 +64,9 @@ struct MathsHandler: APIGatewayLambdaFunction {
let operands = try await request.decode(as: Operands.self, context: context)
return Result(result: operands.lhs / operands.rhs)
}
return router.buildResponder()
let lambda = APIGatewayV2LambdaFunction(
router: router
)
try await lambda.runService()
}

func shutdown() async throws {}
}
40 changes: 3 additions & 37 deletions Sources/HummingbirdLambda/APIGatewayLambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,9 @@ import Hummingbird
import NIOCore
import NIOHTTP1

/// Protocol for Hummingbird Lambdas that use APIGateway
///
/// With this protocol you no longer need to set the `Event` and `Output`
/// associated values.
/// ```swift
/// struct MyLambda: APIGatewayLambda {
/// typealias Context = MyLambdaRequestContext
///
/// init(context: LambdaInitializationContext) {}
///
/// /// build responder that will create a response from a request
/// func buildResponder() -> some Responder<Context> {
/// let router = Router(context: Context.self)
/// router.get("hello") { _,_ in
/// "Hello"
/// }
/// return router.buildResponder()
/// }
/// }
/// ```
public protocol APIGatewayLambdaFunction: LambdaFunction where Event == APIGatewayRequest, Output == APIGatewayResponse {
associatedtype Context = BasicLambdaRequestContext<APIGatewayRequest>
}

extension LambdaFunction where Event == APIGatewayRequest {
/// Specialization of Lambda.request where `Event` is `APIGatewayRequest`
public func request(context: LambdaContext, from: Event) throws -> Request {
try Request(context: context, from: from)
}
}

extension LambdaFunction where Output == APIGatewayResponse {
/// Specialization of Lambda.request where `Output` is `APIGatewayResponse`
public func output(from response: Response) async throws -> Output {
try await response.apiResponse()
}
}
/// Typealias for Lambda function triggered by APIGateway
public typealias APIGatewayLambdaFunction<Responder: HTTPResponder> = LambdaFunction<Responder, APIGatewayRequest, APIGatewayResponse>
where Responder.Context: InitializableFromSource<LambdaRequestContextSource<APIGatewayRequest>>

// conform `APIGatewayRequest` to `APIRequest` so we can use Request.init(context:application:from)
extension APIGatewayRequest: APIRequest {
Expand Down
40 changes: 3 additions & 37 deletions Sources/HummingbirdLambda/APIGatewayV2Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,9 @@ import Hummingbird
import NIOCore
import NIOHTTP1

/// Protocol for Hummingbird Lambdas that use APIGatewayV2
///
/// With this protocol you no longer need to set the `Event` and `Output`
/// associated values.
/// ```swift
/// struct MyLambda: APIGatewayLambda {
/// typealias Context = MyLambdaRequestContext
///
/// init(context: LambdaInitializationContext) {}
///
/// /// build responder that will create a response from a request
/// func buildResponder() -> some Responder<Context> {
/// let router = Router(context: Context.self)
/// router.get("hello") { _,_ in
/// "Hello"
/// }
/// return router.buildResponder()
/// }
/// }
/// ```
public protocol APIGatewayV2LambdaFunction: LambdaFunction where Event == APIGatewayV2Request, Output == APIGatewayV2Response {
associatedtype Context = BasicLambdaRequestContext<APIGatewayV2Request>
}

extension LambdaFunction where Event == APIGatewayV2Request {
/// Specialization of Lambda.request where `Event` is `APIGatewayV2Request`
public func request(context: LambdaContext, from: Event) throws -> Request {
try Request(context: context, from: from)
}
}

extension LambdaFunction where Output == APIGatewayV2Response {
/// Specialization of Lambda.request where `Output` is `APIGatewayV2Response`
public func output(from response: Response) async throws -> Output {
try await response.apiResponse()
}
}
/// Typealias for Lambda function triggered by APIGatewayV2
public typealias APIGatewayV2LambdaFunction<Responder: HTTPResponder> = LambdaFunction<Responder, APIGatewayV2Request, APIGatewayV2Response>
where Responder.Context: InitializableFromSource<LambdaRequestContextSource<APIGatewayV2Request>>

// conform `APIGatewayV2Request` to `APIRequest` so we can use Request.init(context:application:from)
extension APIGatewayV2Request: APIRequest {
Expand Down
8 changes: 2 additions & 6 deletions Sources/HummingbirdLambda/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,8 @@
// temporarily to ease transition from the old symbols that included the "HB"
// prefix to the new ones.

@_documentation(visibility: internal) @available(*, unavailable, renamed: "LambdaFunction")
public typealias HBLambda = LambdaFunction
@_documentation(visibility: internal) @available(*, unavailable, renamed: "APIGatewayLambdaFunction")
public typealias HBAPIGatewayLambda = APIGatewayLambdaFunction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we forward this to APIGatewayLambdaFunction?

@_documentation(visibility: internal) @available(*, unavailable, renamed: "APIGatewayV2LambdaFunction")
public typealias HBAPIGatewayV2Lambda = APIGatewayV2LambdaFunction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this to APIGatewayV2LambdaFunction?

@_documentation(visibility: internal) @available(*, unavailable, renamed: "LambdaFunctionProtocol")
public typealias HBLambda = LambdaFunctionProtocol
@_documentation(visibility: internal) @available(*, unavailable, renamed: "LambdaRequestContext")
public typealias HBLambdaRequestContext = LambdaRequestContext
@_documentation(visibility: internal) @available(*, unavailable, renamed: "BasicLambdaRequestContext")
Expand Down
188 changes: 133 additions & 55 deletions Sources/HummingbirdLambda/LambdaFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,74 +13,152 @@
//===----------------------------------------------------------------------===//

import AWSLambdaEvents
import AWSLambdaRuntimeCore
import AWSLambdaRuntime
import Hummingbird
import Logging
import NIOCore
import NIOPosix
import ServiceLifecycle
import UnixSignals

/// Protocol for Hummingbird Lambdas.
///
/// Defines the `Event` and `Output` types, how you convert from `Event` to ``HummingbirdCore/Request``
/// and ``HummingbirdCore/Response`` to `Output`. Create a type conforming to this protocol and tag it
/// with `@main`.
/// ```swift
/// struct MyLambda: LambdaFunction {
/// typealias Event = APIGatewayRequest
/// typealias Output = APIGatewayResponse
/// typealias Context = MyLambdaRequestContext // must conform to `LambdaRequestContext`
///
/// init(context: LambdaInitializationContext) {}
///
/// /// build responder that will create a response from a request
/// func buildResponder() -> some Responder<Context> {
/// let router = Router(context: Context.self)
/// router.get("hello") { _,_ in
/// "Hello"
/// }
/// return router.buildResponder()
/// }
/// }
/// ```
/// - SeeAlso: ``APIGatewayLambdaFunction`` and ``APIGatewayV2LambdaFunction`` for specializations of this protocol.
public protocol LambdaFunction: Sendable {
/// Lambda event type that can generate HTTP Request
public protocol LambdaEvent: Decodable {
func request(context: LambdaContext) throws -> Request
}

/// Lambda output type that can be generated from HTTP Response
public protocol LambdaOutput: Encodable {
init(from: Response) async throws
}

/// Protocol for a AWS Lambda function.
public protocol LambdaFunctionProtocol: Service where Responder.Context: InitializableFromSource<LambdaRequestContextSource<Event>> {
/// Event that triggers the lambda
associatedtype Event: Decodable
/// Request context
associatedtype Context: InitializableFromSource<LambdaRequestContextSource<Event>> = BasicLambdaRequestContext<Event>
associatedtype Event: LambdaEvent
/// Output of lambda
associatedtype Output: Encodable
/// HTTP Responder
associatedtype Responder: HTTPResponder<Context>
associatedtype Output: LambdaOutput
/// Responder that generates a response from a request and context
associatedtype Responder: HTTPResponder
/// Context passed with Request to responder
typealias Context = Responder.Context

/// Build the responder
var responder: Responder { get async throws }
/// Logger
var logger: Logger { get }
/// services attached to the lambda.
var services: [any Service] { get }
}

extension LambdaFunctionProtocol {
/// Default to no extra services attached to the application.
public var services: [any Service] { [] }
/// Default logger.
public var logger: Logger { .init(label: "Hummingbird") }
}

/// Conform to `Service` from `ServiceLifecycle`.
extension LambdaFunctionProtocol {
/// Construct lambda runtime and run it
public func run() async throws {
let responder = try await self.responder
let runtime = LambdaRuntime { (event: Event, context: LambdaContext) -> Output in
let request = try event.request(context: context)
let context = Responder.Context(source: .init(event: event, lambdaContext: context))
let response = try await responder.respond(to: request, context: context)
return try await .init(from: response)
}
let services: [any Service] = self.services + [LambdaRuntimeService(runtime: runtime, logger: self.logger)]
let serviceGroup = ServiceGroup(
configuration: .init(services: services, logger: self.logger)
)
try await serviceGroup.run()
}

func buildResponder() -> Responder
/// Helper function that runs lambda inside a ServiceGroup which will gracefully
/// shutdown on signals SIGTERM
public func runService() async throws {
let serviceGroup = ServiceGroup(
configuration: .init(
services: [self],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: self.logger
)
)
try await serviceGroup.run()
}
}

/// Initialize application.
init(context: LambdaInitializationContext) async throws
/// Concrete Lambda function
public struct LambdaFunction<Responder: HTTPResponder, Event: LambdaEvent, Output: LambdaOutput>: LambdaFunctionProtocol
where Responder.Context: InitializableFromSource<LambdaRequestContextSource<Event>> {
/// routes requests to responders based on URI
public let responder: Responder
/// services attached to the application.
public var services: [any Service]
/// Logger
public var logger: Logger

/// Called when Lambda is terminating. This is where you can cleanup any resources
func shutdown() async throws
/// Initialize LambdaFunction
/// - Parameters:
/// - responder: HTTP responder
/// - event: Lambda event type that will trigger lambda
/// - output: Lambda output type
/// - services: Services attached to LambdaFunction
/// - logger: Logger used by lambda during setup
public init(
responder: Responder,
event: Event.Type = Event.self,
output: Output.Type = Output.self,
services: [Service] = [],
logger: Logger? = nil
) {
if let logger {
self.logger = logger
} else {
var logger = Logger(label: "Hummingbird")
logger.logLevel = Environment().get("LOG_LEVEL").map { Logger.Level(rawValue: $0) ?? .info } ?? .info
self.logger = logger
}
self.responder = responder
self.services = services
}

/// Convert from `In` type to `Request`
/// Initialize LambdaFunction
/// - Parameters:
/// - context: Lambda context
/// - from: input type
func request(context: LambdaContext, from: Event) throws -> Request
/// - router: HTTP responder builder
/// - event: Lambda event type that will trigger lambda
/// - output: Lambda output type
/// - services: Services attached to LambdaFunction
/// - logger: Logger used by lambda during setup
public init<ResponderBuilder: HTTPResponderBuilder>(
router: ResponderBuilder,
event: Event.Type = Event.self,
output: Output.Type = Output.self,
services: [Service] = [],
logger: Logger? = nil
) where Responder == ResponderBuilder.Responder {
self.init(
responder: router.buildResponder(),
services: services,
logger: logger
)
}

/// Convert from `Response` to `Out` type
/// - Parameter from: response from Hummingbird
func output(from: Response) async throws -> Output
/// Add service to be managed by lambda function's ServiceGroup
/// - Parameter services: list of services to be added
public mutating func addServices(_ services: any Service...) {
self.services.append(contentsOf: services)
}
}

extension LambdaFunction {
/// Initializes and runs the Lambda function.
///
/// If you precede your `EventLoopLambdaHandler` conformer's declaration with the
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// attribute, the system calls the conformer's `main()` method to launch the lambda function.
public static func main() throws {
LambdaFunctionHandler<Self>.main()
}
private struct LambdaRuntimeService<Handler: StreamingLambdaHandler>: Service {
let runtime: LambdaRuntime<Handler>
let logger: Logger

public func shutdown() async throws {}
func run() async throws {
try await cancelWhenGracefulShutdown {
try await self.runtime.run()
}
self.logger.info("Shutting down Hummingbird")
}
}
Loading
Loading