Skip to content
Merged
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
File renamed without changes.
2 changes: 1 addition & 1 deletion .github/workflows/swift-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
image:
- swift:5.10.1
- swift:6.1.2
container:
image: ${{ matrix.image }}
# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest
Expand Down
4 changes: 4 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [BreezeLambdaWebHook]
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ test:
coverage:
llvm-cov export $(TEST_PACKAGE) \
--instr-profile=$(SWIFT_BIN_PATH)/codecov/default.profdata \
--format=lcov > $(GITHUB_WORKSPACE)/lcov.info
--format=lcov > $(GITHUB_WORKSPACE)/lcov.info

preview_docc_lambda_api:
swift package --disable-sandbox preview-documentation --target BreezeLambdaWebHook

26 changes: 19 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
// swift-tools-version: 5.7
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "BreezeLambdaWebHook",
platforms: [
.macOS(.v13),
.iOS(.v15)
.macOS(.v15)
],
products: [
.library(
name: "BreezeLambdaWebHook",
targets: ["BreezeLambdaWebHook"]
),
.executable(
name: "BreezeDemoHTTPApplication",
targets: ["BreezeDemoHTTPApplication"]
)
],
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-events.git", from: "0.1.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.11.2"),
.package(url: "https://github.com/andrea-scuderi/swift-aws-lambda-runtime.git", branch: "main"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.5.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.target(
Expand All @@ -29,10 +34,17 @@ let package = Package(
.product(name: "AsyncHTTPClient", package: "async-http-client"),
]
),
.executableTarget(
name: "BreezeDemoHTTPApplication",
dependencies: [
"BreezeLambdaWebHook"
]
),
.testTarget(
name: "BreezeLambdaWebHookTests",
dependencies: [
.product(name: "AWSLambdaTesting", package: "swift-aws-lambda-runtime"),
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle"),
"BreezeLambdaWebHook"
],
resources: [.copy("Fixtures")]
Expand Down
63 changes: 63 additions & 0 deletions Sources/BreezeDemoHTTPApplication/BreezeDemoHTTPApplication.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2024 (c) Andrea Scuderi - https://github.com/swift-serverless
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import BreezeLambdaWebHook
import AWSLambdaEvents
import AWSLambdaRuntime
import AsyncHTTPClient
import Logging
import NIOCore

/// This is a simple example of a Breeze Lambda WebHook handler.
/// It uses the BreezeHTTPClientService to make an HTTP request to example.com
/// and returns the response body as a string.
struct DemoLambdaHandler: BreezeLambdaWebHookHandler, Sendable {
var handlerContext: HandlerContext

init(handlerContext: HandlerContext) {
self.handlerContext = handlerContext
}

func handle(_ event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response {
context.logger.info("Received event: \(event)")
let request = HTTPClientRequest(url: "https://example.com")
let response = try await handlerContext.httpClient.execute(request, timeout: .seconds(5))
let bytes = try await response.body.collect(upTo: 1024 * 1024) // 1 MB Buffer
let body = String(buffer: bytes)
context.logger.info("Response body: \(body)")
return APIGatewayV2Response(with: body, statusCode: .ok)
}
}

/// This is the main entry point for the Breeze Lambda WebHook application.
/// It creates an instance of the BreezeHTTPApplication and runs it.
/// The application name is used for logging and metrics.
/// The timeout is used to set the maximum time allowed for the Lambda function to run.
/// The default timeout is 30 seconds, but it can be changed to any value.
///
/// Local Testing:
///
/// The application will listen for incoming HTTP requests on port 7000 when run locally.
///
/// Use CURL to invoke the Lambda function, passing a JSON file containg API Gateway V2 request:
///
/// `curl -X POST 127.0.0.1:7000/invoke -H "Content-Type: application/json" -d @Tests/BreezeLambdaWebHookTests/Fixtures/get_webhook_api_gtw.json`
@main
struct BreezeDemoHTTPApplication {

static func main() async throws {
let lambda = BreezeLambdaWebHook<DemoLambdaHandler>(name: "DemoLambdaHandler")
try await lambda.run()
}
}
24 changes: 16 additions & 8 deletions Sources/BreezeLambdaWebHook/APIGatewayV2Response+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,16 @@ import struct AWSLambdaEvents.APIGatewayV2Response
import HTTPTypes
import class Foundation.JSONEncoder

/// Extensions for `APIGatewayV2Response` to simplify response creation
public extension APIGatewayV2Response {
private static let encoder = JSONEncoder()

/// defaultHeaders
/// Override the headers in APIGatewayV2Response
static var defaultHeaders = [ "Content-Type": "application/json" ]

/// Body of an error response
struct BodyError: Codable {
public let error: String
}

/// init
/// Initializer with body error and status code
/// - Parameters:
/// - error: Error
/// - statusCode: HTTP Status Code
Expand All @@ -36,18 +34,28 @@ public extension APIGatewayV2Response {
self.init(with: bodyError, statusCode: statusCode)
}

/// init
/// Initializer with decodable object, status code, and headers
/// - Parameters:
/// - object: Encodable Object
/// - statusCode: HTTP Status Code
init<Output: Encodable>(with object: Output, statusCode: HTTPResponse.Status) {
/// - headers: HTTP Headers
/// - Returns: APIGatewayV2Response
///
/// This initializer encodes the object to JSON and sets it as the body of the response.
/// If encoding fails, it defaults to an empty JSON object.
/// - Note: The `Content-Type` header is set to `application/json` by default.
init<Output: Encodable>(
with object: Output,
statusCode: HTTPResponse.Status,
headers: [String: String] = [ "Content-Type": "application/json" ]
) {
var body = "{}"
if let data = try? Self.encoder.encode(object) {
body = String(data: data, encoding: .utf8) ?? body
}
self.init(
statusCode: statusCode,
headers: APIGatewayV2Response.defaultHeaders,
headers: headers,
body: body,
isBase64Encoded: false
)
Expand Down
36 changes: 36 additions & 0 deletions Sources/BreezeLambdaWebHook/BreezeHTTPClientConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2024 (c) Andrea Scuderi - https://github.com/swift-serverless
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Logging
import NIOCore

/// Error types for BreezeClientService
public enum BreezeClientServiceError: Error {
/// The handler is invalid or not set
case invalidHandler
}

/// Configuration for the Breeze HTTP Client
public struct BreezeHTTPClientConfig: Sendable {
public init(
timeout: TimeAmount = .seconds(30),
logger: Logger = Logger(label: "BreezeLambdaWebHookLogger")
) {
self.timeout = timeout
self.logger = logger
}

public let timeout: TimeAmount
public let logger: Logger
}
105 changes: 50 additions & 55 deletions Sources/BreezeLambdaWebHook/BreezeLambdaWebHook.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 (c) Andrea Scuderi - https://github.com/swift-serverless
// Copyright 2024 (c) Andrea Scuderi - https://github.com/swift-serverless
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -12,65 +12,60 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import AsyncHTTPClient
import AWSLambdaEvents
import AWSLambdaRuntime
import AWSLambdaRuntimeCore
import Foundation
import ServiceLifecycle
import Logging
import NIOCore

public extension LambdaInitializationContext {
enum WebHook {
public static var timeout: Int64 = 30
}
}

public struct HandlerContext {
public let handler: String?
public let httpClient: HTTPClient
}

public class BreezeLambdaWebHook<Handler: BreezeLambdaWebHookHandler>: LambdaHandler {
public typealias Event = APIGatewayV2Request
public typealias Output = APIGatewayV2Response
/// The Service that handles Breeze Lambda WebHook functionality.
public struct BreezeLambdaWebHook<LambdaHandler: BreezeLambdaWebHookHandler>: Service {

let handlerContext: HandlerContext

public required init(context: LambdaInitializationContext) async throws {
let handler = Lambda.env("_HANDLER")
context.logger.info("handler: \(handler ?? "")")

let timeout = HTTPClient.Configuration.Timeout(
connect: .seconds(LambdaInitializationContext.WebHook.timeout),
read: .seconds(LambdaInitializationContext.WebHook.timeout)
)

let configuration = HTTPClient.Configuration(timeout: timeout)
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(context.eventLoop),
configuration: configuration
/// The name of the service, used for logging and identification.
public let name: String
/// Configuration for the Breeze HTTP Client.
public let config: BreezeHTTPClientConfig

/// Initializes a new instance of with the given name and configuration.
/// - Parameters:
/// - name: The name of the service.
/// - config: Configuration for the Breeze HTTP Client.
///
/// This initializer sets up the Breeze Lambda WebHook service with a specified name and configuration.
///
/// - Note: If no configuration is provided, a default configuration with a 30-second timeout and a logger will be used.
public init(
name: String,
config: BreezeHTTPClientConfig? = nil
) {
self.name = name
let defaultConfig = BreezeHTTPClientConfig(
timeout: .seconds(30),
logger: Logger(label: "\(name)")
)

handlerContext = HandlerContext(handler: handler, httpClient: httpClient)

context.terminator.register(name: "shutdown") { eventLoop in
context.logger.info("shutdown: started")
let promise = eventLoop.makePromise(of: Void.self)
Task {
do {
try await self.handlerContext.httpClient.shutdown()
promise.succeed()
context.logger.info("shutdown: succeed")
} catch {
promise.fail(error)
context.logger.info("shutdown: fail")
}
}
return promise.futureResult
}
self.config = config ?? defaultConfig
}

public func handle(_ event: AWSLambdaEvents.APIGatewayV2Request, context: AWSLambdaRuntimeCore.LambdaContext) async throws -> AWSLambdaEvents.APIGatewayV2Response {
return await Handler(handlerContext: handlerContext).handle(context: context, event: event)

/// Runs the Breeze Lambda WebHook service.
/// - Throws: An error if the service fails to start or run.
///
/// This method initializes the Breeze Lambda WebHook service and starts it,
/// handling any errors that may occur during the process.
/// It gracefully shuts down the service on termination signals.
public func run() async throws {
do {
let lambdaService = BreezeLambdaWebHookService<LambdaHandler>(
config: config
)
let serviceGroup = ServiceGroup(
services: [lambdaService],
gracefulShutdownSignals: [.sigterm, .sigint],
logger: config.logger
)
config.logger.error("Starting \(name) ...")
try await serviceGroup.run()
} catch {
config.logger.error("Error running \(name): \(error.localizedDescription)")
}
}
}

6 changes: 6 additions & 0 deletions Sources/BreezeLambdaWebHook/BreezeLambdaWebHookError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif

/// Error types for BreezeLambdaWebHook
public enum BreezeLambdaWebHookError: Error {
/// The request is invalid or malformed
case invalidRequest
}
Loading