Skip to content

Commit

Permalink
feat: Add setProviderAndWait (#30)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
fabriziodemaria authored Jan 29, 2024
1 parent 053dabc commit 3ce6b8d
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 16 deletions.
28 changes: 12 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,17 @@ and in the target dependencies section add:
```swift
import OpenFeature

// Configure your custom `FeatureProvider` and pass it to OpenFeatureAPI
let customProvider = MyCustomProvider()
OpenFeatureAPI.shared.setProvider(provider: customProvider)

// Configure your evaluation context and pass it to OpenFeatureAPI
let ctx = MutableContext(
targetingKey: userId,
structure: MutableStructure(attributes: ["product": Value.string(productId)]))
OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx)

// Get client from OpenFeatureAPI and evaluate your flags
let client = OpenFeatureAPI.shared.getClient()
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
Task {
let provider = CustomProvider()
// configure a provider, wait for it to complete its initialization tasks
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)

// get a bool flag value
let client = OpenFeatureAPI.shared.getClient()
let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false)
}
```

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).

## 🌟 Features


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

```swift
OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx)
await OpenFeatureAPI.shared.setProviderAndWait(provider: MyProvider())
```

> Asynchronous API that doesn't wait is also available
### Targeting

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.
Expand Down
27 changes: 27 additions & 0 deletions Sources/OpenFeature/OpenFeatureAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,30 @@ public class OpenFeatureAPI {
.eraseToAnyPublisher()
}
}

extension OpenFeatureAPI {
public func setProviderAndWait(provider: FeatureProvider) async {
await setProviderAndWait(provider: provider, initialContext: nil)
}

public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async {
let task = Task {
var holder: [AnyCancellable] = []
await withCheckedContinuation { continuation in
let stateObserver = provider.observe().sink {
if $0 == .ready {
continuation.resume()
holder.removeAll()
}
}
stateObserver.store(in: &holder)
setProvider(provider: provider, initialContext: initialContext)
}
}
await withTaskCancellationHandler {
await task.value
} onCancel: {
task.cancel()
}
}
}
33 changes: 33 additions & 0 deletions Tests/OpenFeatureTests/DeveloperExperienceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ final class DeveloperExperienceTests: XCTestCase {
XCTAssertNotNil(eventState)
}

func testSetProviderAndWait() async {
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
withExtendedLifetime(
OpenFeatureAPI.shared.observe().sink { event in
switch event {
case ProviderEvent.ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
})
{
let initCompleteExpectation = XCTestExpectation()

let eventHandler = EventHandler(.stale)
let provider = InjectableEventHandlerProvider(eventHandler: eventHandler)
Task {
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
wait(for: [readyExpectation], timeout: 0)
initCompleteExpectation.fulfill()
}
wait(for: [staleExpectation], timeout: 1)
eventHandler.send(.ready)
wait(for: [initCompleteExpectation], timeout: 2)
}
}

func testClientHooks() {
OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
let client = OpenFeatureAPI.shared.getClient()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation
import OpenFeature
import Combine

class InjectableEventHandlerProvider: FeatureProvider {
public static let name = "InjectableEventHandler"
private let eventHandler: EventHandler

init(eventHandler: EventHandler) {
self.eventHandler = eventHandler
}

func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
// Emit stale, then let the parent test control events via eventHandler
eventHandler.send(.stale)
}

func initialize(initialContext: OpenFeature.EvaluationContext?) {
// Emit stale, then let the parent test control events via eventHandler
eventHandler.send(.stale)
}

var hooks: [any OpenFeature.Hook] = []
var metadata: OpenFeature.ProviderMetadata = InjectableEventHandlerMetadata()

func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
-> ProviderEvaluation<
Bool
>
{
return ProviderEvaluation(value: !defaultValue)
}

func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
-> ProviderEvaluation<
String
>
{
return ProviderEvaluation(value: String(defaultValue.reversed()))
}

func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
-> ProviderEvaluation<
Int64
>
{
return ProviderEvaluation(value: defaultValue * 100)
}

func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
-> ProviderEvaluation<
Double
>
{
return ProviderEvaluation(value: defaultValue * 100)
}

func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
-> ProviderEvaluation<
Value
>
{
return ProviderEvaluation(value: .null)
}

func observe() -> AnyPublisher<OpenFeature.ProviderEvent, Never> {
eventHandler.observe()
}

public struct InjectableEventHandlerMetadata: ProviderMetadata {
public var name: String? = InjectableEventHandlerProvider.name
}
}

0 comments on commit 3ce6b8d

Please sign in to comment.