Skip to content

Add HTTP client transport #8

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

Merged
merged 1 commit into from
Jan 27, 2025
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
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ 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(
name: "OFREP",
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(
Expand Down
89 changes: 89 additions & 0 deletions Sources/OFREP/OFREPHTTPClientTransport.swift
Original file line number Diff line number Diff line change
@@ -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<OFREPHTTPClientTransport> {
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)
)
}
}
28 changes: 28 additions & 0 deletions Tests/OFREPTests/Helpers/ServiceGroup+ShutdownTrigger.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
18 changes: 18 additions & 0 deletions Tests/OFREPTests/Helpers/URL+Stub.swift
Original file line number Diff line number Diff line change
@@ -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")!
}
83 changes: 83 additions & 0 deletions Tests/OFREPTests/OFREPHTTPClientTransportTests.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
16 changes: 1 addition & 15 deletions Tests/OFREPTests/OFREPProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,24 +153,14 @@ 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)

await #expect(transport.numberOfShutdownCalls == 0)

let group = ServiceGroup(
configuration: .init(
services: [
.init(service: provider),
.init(service: ShutdownTrigger(), successTerminationBehavior: .gracefullyShutdownGroup),
],
services: [.init(service: provider), .shutdownTrigger],
logger: Logger(label: "test")
)
)
Expand Down Expand Up @@ -281,7 +271,3 @@ extension OFREPProvider<ClosureOFREPClientTransport> {
self.init(serverURL: .stub, transport: transport)
}
}

extension URL {
fileprivate static let stub = URL(string: "http://stub.stub")!
}