Skip to content

Commit 3ce6b8d

Browse files
feat: Add setProviderAndWait (#30)
Adds `setProviderAndWait` extension function, exposed by this library as a user-facing API (documentation also updated). The application can now use `async/await` to wait for the Provider to be ready, before reading flags. The older alternative (still available) is for the application to call `setProvider` and listen for `.ready` event manually. --------- Signed-off-by: Fabrizio Demaria <[email protected]>
1 parent 053dabc commit 3ce6b8d

File tree

4 files changed

+145
-16
lines changed

4 files changed

+145
-16
lines changed

README.md

+12-16
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,17 @@ and in the target dependencies section add:
7676
```swift
7777
import OpenFeature
7878

79-
// Configure your custom `FeatureProvider` and pass it to OpenFeatureAPI
80-
let customProvider = MyCustomProvider()
81-
OpenFeatureAPI.shared.setProvider(provider: customProvider)
82-
83-
// Configure your evaluation context and pass it to OpenFeatureAPI
84-
let ctx = MutableContext(
85-
targetingKey: userId,
86-
structure: MutableStructure(attributes: ["product": Value.string(productId)]))
87-
OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx)
88-
89-
// Get client from OpenFeatureAPI and evaluate your flags
90-
let client = OpenFeatureAPI.shared.getClient()
91-
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
79+
Task {
80+
let provider = CustomProvider()
81+
// configure a provider, wait for it to complete its initialization tasks
82+
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
83+
84+
// get a bool flag value
85+
let client = OpenFeatureAPI.shared.getClient()
86+
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
87+
}
9288
```
9389

94-
Setting a new provider or setting a new evaluation context might trigger asynchronous operations (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `ProviderReady` event has been sent (see [Eventing](#eventing) below).
95-
9690
## 🌟 Features
9791

9892

@@ -118,9 +112,11 @@ If the provider you're looking for hasn't been created yet, see the [develop a p
118112
Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
119113

120114
```swift
121-
OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx)
115+
await OpenFeatureAPI.shared.setProviderAndWait(provider: MyProvider())
122116
```
123117

118+
> Asynchronous API that doesn't wait is also available
119+
124120
### Targeting
125121

126122
Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location.

Sources/OpenFeature/OpenFeatureAPI.swift

+27
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,30 @@ public class OpenFeatureAPI {
8585
.eraseToAnyPublisher()
8686
}
8787
}
88+
89+
extension OpenFeatureAPI {
90+
public func setProviderAndWait(provider: FeatureProvider) async {
91+
await setProviderAndWait(provider: provider, initialContext: nil)
92+
}
93+
94+
public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async {
95+
let task = Task {
96+
var holder: [AnyCancellable] = []
97+
await withCheckedContinuation { continuation in
98+
let stateObserver = provider.observe().sink {
99+
if $0 == .ready {
100+
continuation.resume()
101+
holder.removeAll()
102+
}
103+
}
104+
stateObserver.store(in: &holder)
105+
setProvider(provider: provider, initialContext: initialContext)
106+
}
107+
}
108+
await withTaskCancellationHandler {
109+
await task.value
110+
} onCancel: {
111+
task.cancel()
112+
}
113+
}
114+
}

Tests/OpenFeatureTests/DeveloperExperienceTests.swift

+33
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,39 @@ final class DeveloperExperienceTests: XCTestCase {
4949
XCTAssertNotNil(eventState)
5050
}
5151

52+
func testSetProviderAndWait() async {
53+
let readyExpectation = XCTestExpectation(description: "Ready")
54+
let errorExpectation = XCTestExpectation(description: "Error")
55+
let staleExpectation = XCTestExpectation(description: "Stale")
56+
withExtendedLifetime(
57+
OpenFeatureAPI.shared.observe().sink { event in
58+
switch event {
59+
case ProviderEvent.ready:
60+
readyExpectation.fulfill()
61+
case ProviderEvent.error:
62+
errorExpectation.fulfill()
63+
case ProviderEvent.stale:
64+
staleExpectation.fulfill()
65+
default:
66+
XCTFail("Unexpected event")
67+
}
68+
})
69+
{
70+
let initCompleteExpectation = XCTestExpectation()
71+
72+
let eventHandler = EventHandler(.stale)
73+
let provider = InjectableEventHandlerProvider(eventHandler: eventHandler)
74+
Task {
75+
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
76+
wait(for: [readyExpectation], timeout: 0)
77+
initCompleteExpectation.fulfill()
78+
}
79+
wait(for: [staleExpectation], timeout: 1)
80+
eventHandler.send(.ready)
81+
wait(for: [initCompleteExpectation], timeout: 2)
82+
}
83+
}
84+
5285
func testClientHooks() {
5386
OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
5487
let client = OpenFeatureAPI.shared.getClient()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Foundation
2+
import OpenFeature
3+
import Combine
4+
5+
class InjectableEventHandlerProvider: FeatureProvider {
6+
public static let name = "InjectableEventHandler"
7+
private let eventHandler: EventHandler
8+
9+
init(eventHandler: EventHandler) {
10+
self.eventHandler = eventHandler
11+
}
12+
13+
func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
14+
// Emit stale, then let the parent test control events via eventHandler
15+
eventHandler.send(.stale)
16+
}
17+
18+
func initialize(initialContext: OpenFeature.EvaluationContext?) {
19+
// Emit stale, then let the parent test control events via eventHandler
20+
eventHandler.send(.stale)
21+
}
22+
23+
var hooks: [any OpenFeature.Hook] = []
24+
var metadata: OpenFeature.ProviderMetadata = InjectableEventHandlerMetadata()
25+
26+
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
27+
-> ProviderEvaluation<
28+
Bool
29+
>
30+
{
31+
return ProviderEvaluation(value: !defaultValue)
32+
}
33+
34+
func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
35+
-> ProviderEvaluation<
36+
String
37+
>
38+
{
39+
return ProviderEvaluation(value: String(defaultValue.reversed()))
40+
}
41+
42+
func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
43+
-> ProviderEvaluation<
44+
Int64
45+
>
46+
{
47+
return ProviderEvaluation(value: defaultValue * 100)
48+
}
49+
50+
func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
51+
-> ProviderEvaluation<
52+
Double
53+
>
54+
{
55+
return ProviderEvaluation(value: defaultValue * 100)
56+
}
57+
58+
func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
59+
-> ProviderEvaluation<
60+
Value
61+
>
62+
{
63+
return ProviderEvaluation(value: .null)
64+
}
65+
66+
func observe() -> AnyPublisher<OpenFeature.ProviderEvent, Never> {
67+
eventHandler.observe()
68+
}
69+
70+
public struct InjectableEventHandlerMetadata: ProviderMetadata {
71+
public var name: String? = InjectableEventHandlerProvider.name
72+
}
73+
}

0 commit comments

Comments
 (0)