Skip to content

Commit

Permalink
Add HTTP client transport
Browse files Browse the repository at this point in the history
  • Loading branch information
slashmo committed Jan 27, 2025
1 parent 6c25020 commit 457e0b6
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 15 deletions.
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")!
}

0 comments on commit 457e0b6

Please sign in to comment.