Skip to content
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

refactor!: Add EventHandler + Combine #29

Merged
merged 15 commits into from
Jan 24, 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
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ 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 emitted (see [Eventing](#eventing) below).
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 Down Expand Up @@ -174,12 +174,13 @@ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGE
Please refer to the documentation of the provider you're using to see what events are supported.

```swift
OpenFeatureAPI.shared.addHandler(
observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready
)

func readyEventEmitted(notification: NSNotification) {
// to something now that the provider is ready
let cancellable = OpenFeatureAPI.shared.observe().sink { event in
switch event {
case ProviderEvent.ready:
// ...
default:
// ...
}
}
```

Expand Down
11 changes: 0 additions & 11 deletions Sources/OpenFeature/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,4 @@ public protocol Client: Features {
/// Hooks are run in the order they're added in the before stage. They are run in reverse order for all
/// other stages.
func addHooks(_ hooks: any Hook...)

/// Add a handler for a particular provider event
/// - Parameter observer: The object observing the event.
/// - Parameter selector: The selector to call for this event.
/// - Parameter event: The event to listen for.
func addHandler(observer: Any, selector: Selector, event: ProviderEvent)

/// Remove a handler for a particular provider event
/// - Parameter observer: The object observing the event.
/// - Parameter event: The event being listened to.
func removeHandler(observer: Any, event: ProviderEvent)
}
28 changes: 28 additions & 0 deletions Sources/OpenFeature/EventHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation
import Combine

public class EventHandler: EventSender, EventPublisher {
private let eventState: CurrentValueSubject<ProviderEvent, Never>

public init(_ state: ProviderEvent) {
eventState = CurrentValueSubject<ProviderEvent, Never>(ProviderEvent.stale)
}

public func observe() -> AnyPublisher<ProviderEvent, Never> {
return eventState.eraseToAnyPublisher()
}

public func send(
_ event: ProviderEvent
) {
eventState.send(event)
}
}

public protocol EventPublisher {
func observe() -> AnyPublisher<ProviderEvent, Never>
}

public protocol EventSender {
func send(_ event: ProviderEvent)
}
58 changes: 20 additions & 38 deletions Sources/OpenFeature/OpenFeatureAPI.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import Foundation
import Combine

/// A global singleton which holds base configuration for the OpenFeature library.
/// Configuration here will be shared across all ``Client``s.
public class OpenFeatureAPI {
private var _provider: FeatureProvider?
private var _provider: FeatureProvider? {
get {
providerSubject.value
}
set {
providerSubject.send(newValue)
}
}
private var _context: EvaluationContext?
private(set) var hooks: [any Hook] = []

private let providerNotificationCentre = NotificationCenter()
private var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)

/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()
Expand All @@ -24,7 +31,6 @@ public class OpenFeatureAPI {
if let context = initialContext {
self._context = context
}

provider.initialize(initialContext: self._context)
}

Expand Down Expand Up @@ -65,41 +71,17 @@ public class OpenFeatureAPI {
public func clearHooks() {
self.hooks.removeAll()
}
}

// MARK: Provider Events

extension OpenFeatureAPI {
public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) {
providerNotificationCentre.addObserver(
observer,
selector: selector,
name: event.notification,
object: nil
)
}

public func removeHandler(observer: Any, event: ProviderEvent) {
providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil)
}

public func emitEvent(
_ event: ProviderEvent,
provider: FeatureProvider,
error: Error? = nil,
details: [AnyHashable: Any]? = nil
) {
var userInfo: [AnyHashable: Any] = [:]
userInfo[providerEventDetailsKeyProvider] = provider

if let error {
userInfo[providerEventDetailsKeyError] = error
public func observe() -> AnyPublisher<ProviderEvent, Never> {
return providerSubject.map { provider in
if let provider = provider {
return provider.observe()
} else {
return Empty<ProviderEvent, Never>()
.eraseToAnyPublisher()
}
}

if let details {
userInfo.merge(details) { $1 } // Merge, keeping value from `details` if any conflicts
}

providerNotificationCentre.post(name: event.notification, object: nil, userInfo: userInfo)
.switchToLatest()
.eraseToAnyPublisher()
}
}
43 changes: 0 additions & 43 deletions Sources/OpenFeature/OpenFeatureClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,11 @@ public class OpenFeatureClient: Client {
private var hookSupport = HookSupport()
private var logger = Logger()

private let providerNotificationCentre = NotificationCenter()

public init(openFeatureApi: OpenFeatureAPI, name: String?, version: String?) {
self.openFeatureApi = openFeatureApi
self.name = name
self.version = version
self.metadata = Metadata(name: name)

subscribeToAllProviderEvents()
}

public func addHooks(_ hooks: any Hook...) {
Expand Down Expand Up @@ -200,42 +196,3 @@ extension OpenFeatureClient {
throw OpenFeatureError.generalError(message: "Unable to match default value type with flag value type")
}
}

// MARK: Events

extension OpenFeatureClient {
public func subscribeToAllProviderEvents() {
ProviderEvent.allCases.forEach { event in
OpenFeatureAPI.shared.addHandler(
observer: self,
selector: #selector(handleProviderEvent(notification:)),
event: event)
}
}

public func unsubscribeFromAllProviderEvents() {
ProviderEvent.allCases.forEach { event in
OpenFeatureAPI.shared.removeHandler(observer: self, event: event)
}
}

@objc public func handleProviderEvent(notification: Notification) {
var userInfo: [AnyHashable: Any] = notification.userInfo ?? [:]
userInfo[providerEventDetailsKeyClient] = self

providerNotificationCentre.post(name: notification.name, object: nil, userInfo: userInfo)
}

public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) {
providerNotificationCentre.addObserver(
observer,
selector: selector,
name: event.notification,
object: nil
)
}

public func removeHandler(observer: Any, event: ProviderEvent) {
providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil)
}
}
2 changes: 1 addition & 1 deletion Sources/OpenFeature/Provider/FeatureProvider.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// The interface implemented by upstream flag providers to resolve flags for their service.
public protocol FeatureProvider {
public protocol FeatureProvider: EventPublisher {
var hooks: [any Hook] { get }
var metadata: ProviderMetadata { get }

Expand Down
10 changes: 8 additions & 2 deletions Sources/OpenFeature/Provider/NoOpProvider.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Foundation
import Combine

/// A ``FeatureProvider`` that simply returns the default values passed to it.
class NoOpProvider: FeatureProvider {
public static let passedInDefault = "Passed in default"
private let eventHandler = EventHandler(.ready)

public enum Mode {
case normal
Expand All @@ -13,11 +15,11 @@ class NoOpProvider: FeatureProvider {
var hooks: [any Hook] = []

func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) {
OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self)
eventHandler.send(.ready)
}

func initialize(initialContext: EvaluationContext?) {
OpenFeatureAPI.shared.emitEvent(.ready, provider: self)
eventHandler.send(.ready)
}

func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
Expand Down Expand Up @@ -64,6 +66,10 @@ class NoOpProvider: FeatureProvider {
return ProviderEvaluation(
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}

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

extension NoOpProvider {
Expand Down
4 changes: 0 additions & 4 deletions Sources/OpenFeature/Provider/ProviderEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,4 @@ public enum ProviderEvent: String, CaseIterable {
case error = "PROVIDER_ERROR"
case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED"
case stale = "PROVIDER_STALE"

var notification: NSNotification.Name {
NSNotification.Name(rawValue)
}
}
30 changes: 30 additions & 0 deletions Tests/OpenFeatureTests/DeveloperExperienceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ final class DeveloperExperienceTests: XCTestCase {
XCTAssertFalse(flagValue)
}

func testObserveGlobalEvents() {
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
var eventState = 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 provider = DoSomethingProvider()
OpenFeatureAPI.shared.setProvider(provider: provider)
wait(for: [readyExpectation], timeout: 5)

// Clearing the Provider shouldn't send further global events from it
// Dropping the first event, which reflects the current state before clearing
eventState = OpenFeatureAPI.shared.observe().dropFirst().sink { _ in
XCTFail("Unexpected event")
}
OpenFeatureAPI.shared.clearProvider()
provider.initialize(initialContext: MutableContext(attributes: ["Test": Value.string("Test")]))
XCTAssertNotNil(eventState)
}

func testClientHooks() {
OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
let client = OpenFeatureAPI.shared.getClient()
Expand Down
Loading