Skip to content

feat: Add setProviderAndWait #30

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 5 commits into from
Jan 29, 2024
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
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
}
}