From 0ac902cc940c45d27e405f4e090dc5ba4f8e6db2 Mon Sep 17 00:00:00 2001 From: Moritz Lang <16192401+slashmo@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:31:45 +0100 Subject: [PATCH] Add HTTP client transport --- Package.swift | 2 + Sources/OFREP/OFREPHTTPClientTransport.swift | 89 +++++++++++++++++++ .../ServiceGroup+ShutdownTrigger.swift | 28 ++++++ Tests/OFREPTests/Helpers/URL+Stub.swift | 18 ++++ .../OFREPHTTPClientTransportTests.swift | 83 +++++++++++++++++ Tests/OFREPTests/OFREPProviderTests.swift | 16 +--- 6 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 Sources/OFREP/OFREPHTTPClientTransport.swift create mode 100644 Tests/OFREPTests/Helpers/ServiceGroup+ShutdownTrigger.swift create mode 100644 Tests/OFREPTests/Helpers/URL+Stub.swift create mode 100644 Tests/OFREPTests/OFREPHTTPClientTransportTests.swift diff --git a/Package.swift b/Package.swift index e2f83e8..d989e51 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,7 @@ let package = Package( .package(url: "https://github.com/swift-open-feature/swift-open-feature.git", branch: "main"), .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.7.0"), .package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.0.0"), + .package(url: "https://github.com/swift-server/swift-openapi-async-http-client.git", from: "1.0.0"), ], targets: [ .target( @@ -18,6 +19,7 @@ let package = Package( dependencies: [ .product(name: "OpenFeature", package: "swift-open-feature"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"), ] ), .testTarget( diff --git a/Sources/OFREP/OFREPHTTPClientTransport.swift b/Sources/OFREP/OFREPHTTPClientTransport.swift new file mode 100644 index 0000000..3b95db3 --- /dev/null +++ b/Sources/OFREP/OFREPHTTPClientTransport.swift @@ -0,0 +1,89 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenFeature open source project +// +// Copyright (c) 2025 the Swift OpenFeature project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AsyncHTTPClient +import Foundation +import HTTPTypes +import Logging +import NIOCore +import OpenAPIAsyncHTTPClient +import OpenAPIRuntime + +struct OFREPHTTPClientTransport: OFREPClientTransport { + let transport: AsyncHTTPClientTransport + let shouldShutDownHTTPClient: Bool + + func send( + _ request: HTTPRequest, + body: HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> ( + HTTPResponse, + HTTPBody? + ) { + try await transport.send(request, body: body, baseURL: baseURL, operationID: operationID) + } + + func shutdownGracefully() async throws { + guard shouldShutDownHTTPClient else { return } + try await transport.configuration.client.shutdown() + } + + static let loggingDisabled = Logger(label: "OFREP-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() }) +} + +extension OFREPProvider { + public init(serverURL: URL, httpClient: HTTPClient = .shared, timeout: Duration = .seconds(60)) { + self.init( + serverURL: serverURL, + transport: AsyncHTTPClientTransport( + configuration: AsyncHTTPClientTransport.Configuration( + client: httpClient, + timeout: TimeAmount(timeout) + ) + ) + ) + } + + public init( + serverURL: URL, + configuration: HTTPClient.Configuration, + eventLoopGroup: EventLoopGroup = HTTPClient.defaultEventLoopGroup, + backgroundActivityLogger: Logger? = nil, + timeout: Duration = .seconds(60) + ) { + let httpClient = HTTPClient( + eventLoopGroupProvider: .shared(eventLoopGroup), + configuration: configuration, + backgroundActivityLogger: backgroundActivityLogger ?? OFREPHTTPClientTransport.loggingDisabled + ) + let httpClientTransport = AsyncHTTPClientTransport( + configuration: AsyncHTTPClientTransport.Configuration( + client: httpClient, + timeout: TimeAmount(timeout) + ) + ) + self.init( + serverURL: serverURL, + transport: OFREPHTTPClientTransport(transport: httpClientTransport, shouldShutDownHTTPClient: true) + ) + } + + package init(serverURL: URL, transport: AsyncHTTPClientTransport) { + self.init( + serverURL: serverURL, + transport: OFREPHTTPClientTransport(transport: transport, shouldShutDownHTTPClient: false) + ) + } +} diff --git a/Tests/OFREPTests/Helpers/ServiceGroup+ShutdownTrigger.swift b/Tests/OFREPTests/Helpers/ServiceGroup+ShutdownTrigger.swift new file mode 100644 index 0000000..e4ec818 --- /dev/null +++ b/Tests/OFREPTests/Helpers/ServiceGroup+ShutdownTrigger.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenFeature open source project +// +// Copyright (c) 2025 the Swift OpenFeature project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import ServiceLifecycle + +private struct ShutdownTriggerService: Service, CustomStringConvertible { + let description = "ShutdownTrigger" + + func run() async throws {} +} + +extension ServiceGroupConfiguration.ServiceConfiguration { + /// A no-op service which is used to shut down the service group upon successful termination. + static let shutdownTrigger = Self( + service: ShutdownTriggerService(), + successTerminationBehavior: .gracefullyShutdownGroup + ) +} diff --git a/Tests/OFREPTests/Helpers/URL+Stub.swift b/Tests/OFREPTests/Helpers/URL+Stub.swift new file mode 100644 index 0000000..f01e6b1 --- /dev/null +++ b/Tests/OFREPTests/Helpers/URL+Stub.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenFeature open source project +// +// Copyright (c) 2025 the Swift OpenFeature project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension URL { + static let stub = URL(string: "http://stub.stub")! +} diff --git a/Tests/OFREPTests/OFREPHTTPClientTransportTests.swift b/Tests/OFREPTests/OFREPHTTPClientTransportTests.swift new file mode 100644 index 0000000..6f4ff86 --- /dev/null +++ b/Tests/OFREPTests/OFREPHTTPClientTransportTests.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift OpenFeature open source project +// +// Copyright (c) 2025 the Swift OpenFeature project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AsyncHTTPClient +import Foundation +import Logging +import NIOCore +import OFREP +import ServiceLifecycle +import Testing + +@testable import OpenAPIAsyncHTTPClient + +@Suite("HTTP Client Transport") +struct OFREPHTTPClientTransportTests { + @Test("Defaults to shared HTTP client") + func sharedHTTPClient() async throws { + let provider = OFREPProvider(serverURL: .stub) + + let serviceGroup = ServiceGroup( + configuration: .init( + services: [.init(service: provider), .shutdownTrigger], + logger: Logger(label: "test") + ) + ) + + try await serviceGroup.run() + } + + @Test("Shuts down internally created HTTP client") + func internallyCreatedHTTPClient() async throws { + let provider = OFREPProvider(serverURL: .stub, configuration: HTTPClient.Configuration()) + + let serviceGroup = ServiceGroup( + configuration: .init( + services: [.init(service: provider), .shutdownTrigger], + logger: Logger(label: "test") + ) + ) + + try await serviceGroup.run() + } + + @Test("Forwards request to AsyncHTTPClientTransport") + func forwardsRequest() async throws { + let requestSender = RecordingRequestSender() + let transport = AsyncHTTPClientTransport(configuration: .init(), requestSender: requestSender) + let provider = OFREPProvider(serverURL: .stub, transport: transport) + + _ = await provider.resolution(of: "flag", defaultValue: false, context: nil) + + await #expect(requestSender.requests.count == 1) + } +} + +private actor RecordingRequestSender: HTTPRequestSending { + var requests = [Request]() + + func send( + request: HTTPClientRequest, + with client: HTTPClient, + timeout: TimeAmount + ) async throws -> AsyncHTTPClientTransport.Response { + requests.append(Request(request: request, client: client, timeout: timeout)) + return HTTPClientResponse() + } + + struct Request { + let request: HTTPClientRequest + let client: HTTPClient + let timeout: TimeAmount + } +} diff --git a/Tests/OFREPTests/OFREPProviderTests.swift b/Tests/OFREPTests/OFREPProviderTests.swift index 3232637..3808945 100644 --- a/Tests/OFREPTests/OFREPProviderTests.swift +++ b/Tests/OFREPTests/OFREPProviderTests.swift @@ -153,13 +153,6 @@ final class OFREPProviderTests { @Test("Graceful shutdown") func shutsDownTransport() async throws { - /// A no-op service which is used to shut down the service group upon successful termination. - struct ShutdownTrigger: Service, CustomStringConvertible { - let description = "ShutdownTrigger" - - func run() async throws {} - } - let transport = RecordingOFREPClientTransport() let provider = OFREPProvider(transport: transport) @@ -167,10 +160,7 @@ final class OFREPProviderTests { let group = ServiceGroup( configuration: .init( - services: [ - .init(service: provider), - .init(service: ShutdownTrigger(), successTerminationBehavior: .gracefullyShutdownGroup), - ], + services: [.init(service: provider), .shutdownTrigger], logger: Logger(label: "test") ) ) @@ -281,7 +271,3 @@ extension OFREPProvider { self.init(serverURL: .stub, transport: transport) } } - -extension URL { - fileprivate static let stub = URL(string: "http://stub.stub")! -}