Skip to content

Commit 457e0b6

Browse files
committed
Add HTTP client transport
1 parent 6c25020 commit 457e0b6

6 files changed

+221
-15
lines changed

Package.swift

+2
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ let package = Package(
1111
.package(url: "https://github.com/swift-open-feature/swift-open-feature.git", branch: "main"),
1212
.package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.7.0"),
1313
.package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.0.0"),
14+
.package(url: "https://github.com/swift-server/swift-openapi-async-http-client.git", from: "1.0.0"),
1415
],
1516
targets: [
1617
.target(
1718
name: "OFREP",
1819
dependencies: [
1920
.product(name: "OpenFeature", package: "swift-open-feature"),
2021
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
22+
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
2123
]
2224
),
2325
.testTarget(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import AsyncHTTPClient
15+
import Foundation
16+
import HTTPTypes
17+
import Logging
18+
import NIOCore
19+
import OpenAPIAsyncHTTPClient
20+
import OpenAPIRuntime
21+
22+
struct OFREPHTTPClientTransport: OFREPClientTransport {
23+
let transport: AsyncHTTPClientTransport
24+
let shouldShutDownHTTPClient: Bool
25+
26+
func send(
27+
_ request: HTTPRequest,
28+
body: HTTPBody?,
29+
baseURL: URL,
30+
operationID: String
31+
) async throws -> (
32+
HTTPResponse,
33+
HTTPBody?
34+
) {
35+
try await transport.send(request, body: body, baseURL: baseURL, operationID: operationID)
36+
}
37+
38+
func shutdownGracefully() async throws {
39+
guard shouldShutDownHTTPClient else { return }
40+
try await transport.configuration.client.shutdown()
41+
}
42+
43+
static let loggingDisabled = Logger(label: "OFREP-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() })
44+
}
45+
46+
extension OFREPProvider<OFREPHTTPClientTransport> {
47+
public init(serverURL: URL, httpClient: HTTPClient = .shared, timeout: Duration = .seconds(60)) {
48+
self.init(
49+
serverURL: serverURL,
50+
transport: AsyncHTTPClientTransport(
51+
configuration: AsyncHTTPClientTransport.Configuration(
52+
client: httpClient,
53+
timeout: TimeAmount(timeout)
54+
)
55+
)
56+
)
57+
}
58+
59+
public init(
60+
serverURL: URL,
61+
configuration: HTTPClient.Configuration,
62+
eventLoopGroup: EventLoopGroup = HTTPClient.defaultEventLoopGroup,
63+
backgroundActivityLogger: Logger? = nil,
64+
timeout: Duration = .seconds(60)
65+
) {
66+
let httpClient = HTTPClient(
67+
eventLoopGroupProvider: .shared(eventLoopGroup),
68+
configuration: configuration,
69+
backgroundActivityLogger: backgroundActivityLogger ?? OFREPHTTPClientTransport.loggingDisabled
70+
)
71+
let httpClientTransport = AsyncHTTPClientTransport(
72+
configuration: AsyncHTTPClientTransport.Configuration(
73+
client: httpClient,
74+
timeout: TimeAmount(timeout)
75+
)
76+
)
77+
self.init(
78+
serverURL: serverURL,
79+
transport: OFREPHTTPClientTransport(transport: httpClientTransport, shouldShutDownHTTPClient: true)
80+
)
81+
}
82+
83+
package init(serverURL: URL, transport: AsyncHTTPClientTransport) {
84+
self.init(
85+
serverURL: serverURL,
86+
transport: OFREPHTTPClientTransport(transport: transport, shouldShutDownHTTPClient: false)
87+
)
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import ServiceLifecycle
15+
16+
private struct ShutdownTriggerService: Service, CustomStringConvertible {
17+
let description = "ShutdownTrigger"
18+
19+
func run() async throws {}
20+
}
21+
22+
extension ServiceGroupConfiguration.ServiceConfiguration {
23+
/// A no-op service which is used to shut down the service group upon successful termination.
24+
static let shutdownTrigger = Self(
25+
service: ShutdownTriggerService(),
26+
successTerminationBehavior: .gracefullyShutdownGroup
27+
)
28+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Foundation
15+
16+
extension URL {
17+
static let stub = URL(string: "http://stub.stub")!
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import AsyncHTTPClient
15+
import Foundation
16+
import Logging
17+
import NIOCore
18+
import OFREP
19+
import ServiceLifecycle
20+
import Testing
21+
22+
@testable import OpenAPIAsyncHTTPClient
23+
24+
@Suite("HTTP Client Transport")
25+
struct OFREPHTTPClientTransportTests {
26+
@Test("Defaults to shared HTTP client")
27+
func sharedHTTPClient() async throws {
28+
let provider = OFREPProvider(serverURL: .stub)
29+
30+
let serviceGroup = ServiceGroup(
31+
configuration: .init(
32+
services: [.init(service: provider), .shutdownTrigger],
33+
logger: Logger(label: "test")
34+
)
35+
)
36+
37+
try await serviceGroup.run()
38+
}
39+
40+
@Test("Shuts down internally created HTTP client")
41+
func internallyCreatedHTTPClient() async throws {
42+
let provider = OFREPProvider(serverURL: .stub, configuration: HTTPClient.Configuration())
43+
44+
let serviceGroup = ServiceGroup(
45+
configuration: .init(
46+
services: [.init(service: provider), .shutdownTrigger],
47+
logger: Logger(label: "test")
48+
)
49+
)
50+
51+
try await serviceGroup.run()
52+
}
53+
54+
@Test("Forwards request to AsyncHTTPClientTransport")
55+
func forwardsRequest() async throws {
56+
let requestSender = RecordingRequestSender()
57+
let transport = AsyncHTTPClientTransport(configuration: .init(), requestSender: requestSender)
58+
let provider = OFREPProvider(serverURL: .stub, transport: transport)
59+
60+
_ = await provider.resolution(of: "flag", defaultValue: false, context: nil)
61+
62+
await #expect(requestSender.requests.count == 1)
63+
}
64+
}
65+
66+
private actor RecordingRequestSender: HTTPRequestSending {
67+
var requests = [Request]()
68+
69+
func send(
70+
request: HTTPClientRequest,
71+
with client: HTTPClient,
72+
timeout: TimeAmount
73+
) async throws -> AsyncHTTPClientTransport.Response {
74+
requests.append(Request(request: request, client: client, timeout: timeout))
75+
return HTTPClientResponse()
76+
}
77+
78+
struct Request {
79+
let request: HTTPClientRequest
80+
let client: HTTPClient
81+
let timeout: TimeAmount
82+
}
83+
}

Tests/OFREPTests/OFREPProviderTests.swift

+1-15
Original file line numberDiff line numberDiff line change
@@ -153,24 +153,14 @@ final class OFREPProviderTests {
153153

154154
@Test("Graceful shutdown")
155155
func shutsDownTransport() async throws {
156-
/// A no-op service which is used to shut down the service group upon successful termination.
157-
struct ShutdownTrigger: Service, CustomStringConvertible {
158-
let description = "ShutdownTrigger"
159-
160-
func run() async throws {}
161-
}
162-
163156
let transport = RecordingOFREPClientTransport()
164157
let provider = OFREPProvider(transport: transport)
165158

166159
await #expect(transport.numberOfShutdownCalls == 0)
167160

168161
let group = ServiceGroup(
169162
configuration: .init(
170-
services: [
171-
.init(service: provider),
172-
.init(service: ShutdownTrigger(), successTerminationBehavior: .gracefullyShutdownGroup),
173-
],
163+
services: [.init(service: provider), .shutdownTrigger],
174164
logger: Logger(label: "test")
175165
)
176166
)
@@ -281,7 +271,3 @@ extension OFREPProvider<ClosureOFREPClientTransport> {
281271
self.init(serverURL: .stub, transport: transport)
282272
}
283273
}
284-
285-
extension URL {
286-
fileprivate static let stub = URL(string: "http://stub.stub")!
287-
}

0 commit comments

Comments
 (0)