Skip to content
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<p align="center" class="github-badges">
<!-- TODO: update this with the version of the SDK your implementation supports -->

<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
<a href="https://github.com/open-feature/spec/releases/tag/v0.8.0">
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
</a>
<!-- x-release-please-start-version -->
Expand Down Expand Up @@ -90,11 +90,12 @@ Task {
## 🌟 Features


| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ❌ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
| ❌ | [Logging](#logging) | Integrate with popular logging packages. |
| ❌ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
Expand Down Expand Up @@ -153,6 +154,10 @@ _ = client.getValue(
defaultValue: false,
options: FlagEvaluationOptions(hooks: [ExampleHook()]))
```
### Tracking

Tracking is not yet available in the iOS SDK.

### Logging

Logging customization is not yet available in the iOS SDK.
Expand Down Expand Up @@ -242,7 +247,7 @@ class BooleanHook: Hook {
// do something
}

func finallyAfter<HookValue>(ctx: HookContext<HookValue>, hints: [String: Any]) {
func finally<HookValue>(ctx: HookContext<HookValue>, hints: [String: Any]) {
// do something
}
}
Expand Down
17 changes: 6 additions & 11 deletions Sources/OpenFeature/EventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,24 @@ import Combine
import Foundation

public class EventHandler: EventSender, EventPublisher {
private let eventState: CurrentValueSubject<ProviderEvent, Never>
private let lastSentEvent = PassthroughSubject<ProviderEvent?, Never>()

convenience init() {
self.init(.notReady)
public init() {
}

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

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

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

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

public protocol EventSender {
Expand Down
10 changes: 8 additions & 2 deletions Sources/OpenFeature/Hook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ public protocol Hook {

func error<HookValue>(ctx: HookContext<HookValue>, error: Error, hints: [String: Any])

func finallyAfter<HookValue>(ctx: HookContext<HookValue>, hints: [String: Any])
func finally<HookValue>(
ctx: HookContext<HookValue>,
details: FlagEvaluationDetails<HookValue>,
hints: [String: Any]
)

func supportsFlagValueType(flagValueType: FlagValueType) -> Bool
}
Expand All @@ -31,7 +35,9 @@ extension Hook {
// Default implementation
}

public func finallyAfter<HookValue>(ctx: HookContext<HookValue>, hints: [String: Any]) {
public func finally<HookValue>(
ctx: HookContext<HookValue>, details: FlagEvaluationDetails<HookValue>, hints: [String: Any]
) {
// Default implementation
}

Expand Down
36 changes: 20 additions & 16 deletions Sources/OpenFeature/HookSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,12 @@ import os
class HookSupport {
var logger = Logger()

func errorHooks<T>(
flagValueType: FlagValueType, hookCtx: HookContext<T>, error: Error, hooks: [any Hook], hints: [String: Any]
) {
hooks
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
.forEach { $0.error(ctx: hookCtx, error: error, hints: hints) }
}

func afterAllHooks<T>(
flagValueType: FlagValueType, hookCtx: HookContext<T>, hooks: [any Hook], hints: [String: Any]
) {
func beforeHooks<T>(flagValueType: FlagValueType, hookCtx: HookContext<T>, hooks: [any Hook], hints: [String: Any])
{
hooks
.reversed()
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
.forEach { $0.finallyAfter(ctx: hookCtx, hints: hints) }
.forEach { $0.before(ctx: hookCtx, hints: hints) }
}

func afterHooks<T>(
Expand All @@ -32,11 +24,23 @@ class HookSupport {
.forEach { $0.after(ctx: hookCtx, details: details, hints: hints) }
}

func beforeHooks<T>(flagValueType: FlagValueType, hookCtx: HookContext<T>, hooks: [any Hook], hints: [String: Any])
{
func errorHooks<T>(
flagValueType: FlagValueType, hookCtx: HookContext<T>, error: Error, hooks: [any Hook], hints: [String: Any]
) {
hooks
.reversed()
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
.forEach { $0.before(ctx: hookCtx, hints: hints) }
.forEach { $0.error(ctx: hookCtx, error: error, hints: hints) }
}

func finallyHooks<T>(
flagValueType: FlagValueType,
hookCtx: HookContext<T>,
details: FlagEvaluationDetails<T>,
hooks: [any Hook],
hints: [String: Any]
) {
hooks
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
.forEach { $0.finally(ctx: hookCtx, details: details, hints: hints) }
}
}
174 changes: 129 additions & 45 deletions Sources/OpenFeature/OpenFeatureAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,107 @@ import Foundation
/// 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? {
get {
providerSubject.value
}
set {
providerSubject.send(newValue)
}
}
private var _context: EvaluationContext?
private let eventHandler = EventHandler()
private let queue = DispatchQueue(label: "com.openfeature.providerDescriptor.queue")

private(set) var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)
private(set) var evaluationContext: EvaluationContext?
private(set) var providerStatus: ProviderStatus = .notReady
private(set) var hooks: [any Hook] = []
private var providerSubject = CurrentValueSubject<FeatureProvider?, Never>(nil)

/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()

public init() {
}

public func setProvider(provider: FeatureProvider) {
self.setProvider(provider: provider, initialContext: nil)
/**
Set provider and calls its `initialize` in a background thread.
Readiness can be determined from `getState` or listening for `ready` event.
*/
public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) {
queue.async {
Task {
await self.setProviderInternal(provider: provider, initialContext: initialContext)
}
}
}

public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) {
self._provider = provider
if let context = initialContext {
self._context = context
/**
Set provider and calls its `initialize`.
This async function returns when the `initialize` from the provider is completed.
*/
public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async {
await withCheckedContinuation { continuation in
queue.async {
Task {
await self.setProviderInternal(provider: provider, initialContext: initialContext)
continuation.resume()
}
}
}
provider.initialize(initialContext: self._context)
}

/**
Set provider and calls its `initialize` in a background thread.
Readiness can be determined from `getState` or listening for `ready` event.
*/
public func setProvider(provider: FeatureProvider) {
setProvider(provider: provider, initialContext: nil)
}

/**
Set provider and calls its `initialize`.
This async function returns when the `initialize` from the provider is completed.
*/
public func setProviderAndWait(provider: FeatureProvider) async {
await setProviderAndWait(provider: provider, initialContext: nil)
}

public func getProvider() -> FeatureProvider? {
return self._provider
return self.providerSubject.value
}

public func clearProvider() {
self._provider = nil
queue.sync {
self.providerSubject.send(nil)
self.providerStatus = .notReady
}
}

/**
Set evaluation context and calls the provider's `onContextSet` in a background thread.
Readiness can be determined from `getState` or listening for `contextChanged` event.
*/
public func setEvaluationContext(evaluationContext: EvaluationContext) {
let oldContext = self._context
self._context = evaluationContext
getProvider()?.onContextSet(oldContext: oldContext, newContext: evaluationContext)
queue.async {
Task {
await self.updateContext(evaluationContext: evaluationContext)
}
}
}

/**
Set evaluation context and calls the provider's `onContextSet`.
This async function returns when the `onContextSet` from the provider is completed.
*/
public func setEvaluationContextAndWait(evaluationContext: EvaluationContext) async {
await withCheckedContinuation { continuation in
queue.async {
Task {
await self.updateContext(evaluationContext: evaluationContext)
continuation.resume()
}
}
}
}

public func getEvaluationContext() -> EvaluationContext? {
return self._context
return self.evaluationContext
}

public func getProviderStatus() -> ProviderStatus {
return self.providerStatus
}

public func getProviderMetadata() -> ProviderMetadata? {
Expand All @@ -72,43 +127,72 @@ public class OpenFeatureAPI {
self.hooks.removeAll()
}

public func observe() -> AnyPublisher<ProviderEvent, Never> {
public func observe() -> AnyPublisher<ProviderEvent?, Never> {
return providerSubject.map { provider in
if let provider = provider {
return provider.observe()
.merge(with: self.eventHandler.observe())
.eraseToAnyPublisher()
} else {
return Empty<ProviderEvent, Never>()
return Empty<ProviderEvent?, Never>()
.eraseToAnyPublisher()
}
}
.switchToLatest()
.eraseToAnyPublisher()
}
}

extension OpenFeatureAPI {
public func setProviderAndWait(provider: FeatureProvider) async {
await setProviderAndWait(provider: provider, initialContext: nil)
internal func getState() -> OpenFeatureState {
return queue.sync {
OpenFeatureState(
provider: providerSubject.value,
evaluationContext: evaluationContext,
providerStatus: providerStatus)
}
}

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 || $0 == .error {
continuation.resume()
holder.removeAll()
}
}
stateObserver.store(in: &holder)
setProvider(provider: provider, initialContext: initialContext)
private func setProviderInternal(provider: FeatureProvider, initialContext: EvaluationContext? = nil) async {
self.providerStatus = .notReady
self.providerSubject.send(provider)

if let initialContext = initialContext {
self.evaluationContext = initialContext
}

do {
try await provider.initialize(initialContext: initialContext)
self.providerStatus = .ready
self.eventHandler.send(.ready)
} catch {
switch error {
case OpenFeatureError.providerFatalError:
self.providerStatus = .fatal
self.eventHandler.send(.error(errorCode: .providerFatal))
default:
self.providerStatus = .error
self.eventHandler.send(.error(message: error.localizedDescription))
}
}
await withTaskCancellationHandler {
await task.value
} onCancel: {
task.cancel()
}

private func updateContext(evaluationContext: EvaluationContext) async {
do {
let oldContext = self.evaluationContext
self.evaluationContext = evaluationContext
self.providerStatus = .reconciling
eventHandler.send(.reconciling)
try await self.providerSubject.value?.onContextSet(oldContext: oldContext, newContext: evaluationContext)
self.providerStatus = .ready
eventHandler.send(.contextChanged)
} catch {
self.providerStatus = .error
eventHandler.send(.error(message: error.localizedDescription))
}
}

struct OpenFeatureState {
let provider: FeatureProvider?
let evaluationContext: EvaluationContext?
let providerStatus: ProviderStatus
}
}
Loading