diff --git a/.gitignore b/.gitignore index cc8936d..8911a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ xcuserdata/ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.swiftpm/xcode/xcshareddata/xcschemes/OpenFeature.xcscheme .netrc .build .mockingbird diff --git a/.swiftlint.yml b/.swiftlint.yml index 1255dc1..38d2c22 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,7 +3,7 @@ excluded: - ${PWD}/Pods - ${PWD}/DerivedData - ${PWD}/.build - - ${PWD}/Tools/.build + - ${PWD}/Tools/*/.build - ${PWD}/Tests/OpenFeatureTests/MockingbirdMocks/ - ${PWD}/Sources/OpenFeature/FlagResolver diff --git a/README.md b/README.md index 7c8f2ce..5a31e10 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ To enable the provider and start resolving flags add the following: import OpenFeature // Change this to your actual provider -OpenFeatureAPI.shared.provider = NoOpProvider() +OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let value = client.getBooleanValue(key: "flag", defaultValue: false) diff --git a/Sources/OpenFeature/Client.swift b/Sources/OpenFeature/Client.swift index 4083a86..49f560f 100644 --- a/Sources/OpenFeature/Client.swift +++ b/Sources/OpenFeature/Client.swift @@ -4,9 +4,6 @@ import Foundation public protocol Client: Features { var metadata: Metadata { get } - /// Return an optional client-level evaluation context. - var evaluationContext: EvaluationContext? { get set } - /// The hooks associated to this client. var hooks: [AnyHook] { get } diff --git a/Sources/OpenFeature/EvaluationContext.swift b/Sources/OpenFeature/EvaluationContext.swift index 1ad50a9..460467f 100644 --- a/Sources/OpenFeature/EvaluationContext.swift +++ b/Sources/OpenFeature/EvaluationContext.swift @@ -4,6 +4,4 @@ public protocol EvaluationContext: Structure { func getTargetingKey() -> String func setTargetingKey(targetingKey: String) - - func merge(overridingContext: EvaluationContext) -> EvaluationContext } diff --git a/Sources/OpenFeature/FeatureProvider.swift b/Sources/OpenFeature/FeatureProvider.swift index 8b8bb2f..4191441 100644 --- a/Sources/OpenFeature/FeatureProvider.swift +++ b/Sources/OpenFeature/FeatureProvider.swift @@ -5,19 +5,25 @@ public protocol FeatureProvider { var hooks: [AnyHook] { get } var metadata: Metadata { get } - func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws -> ProviderEvaluation< + // Called by OpenFeatureAPI whenever the new Provider is registered + func initialize(initialContext: EvaluationContext) async + + // Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application + func onContextSet(oldContext: EvaluationContext, newContext: EvaluationContext) async + + func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> ProviderEvaluation< Bool > - func getStringEvaluation(key: String, defaultValue: String, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getStringEvaluation(key: String, defaultValue: String) throws -> ProviderEvaluation< String > - func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> ProviderEvaluation< Int64 > - func getDoubleEvaluation(key: String, defaultValue: Double, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getDoubleEvaluation(key: String, defaultValue: Double) throws -> ProviderEvaluation< Double > - func getObjectEvaluation(key: String, defaultValue: Value, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getObjectEvaluation(key: String, defaultValue: Value) throws -> ProviderEvaluation< Value > } diff --git a/Sources/OpenFeature/Features.swift b/Sources/OpenFeature/Features.swift index 7a510d2..b0364c6 100644 --- a/Sources/OpenFeature/Features.swift +++ b/Sources/OpenFeature/Features.swift @@ -4,75 +4,55 @@ public protocol Features { // MARK: Bool func getBooleanValue(key: String, defaultValue: Bool) -> Bool - func getBooleanValue(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> Bool - - func getBooleanValue(key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getBooleanValue(key: String, defaultValue: Bool, options: FlagEvaluationOptions) -> Bool func getBooleanDetails(key: String, defaultValue: Bool) -> FlagEvaluationDetails - func getBooleanDetails(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> FlagEvaluationDetails - - func getBooleanDetails(key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getBooleanDetails(key: String, defaultValue: Bool, options: FlagEvaluationOptions) -> FlagEvaluationDetails // MARK: String func getStringValue(key: String, defaultValue: String) -> String - func getStringValue(key: String, defaultValue: String, ctx: EvaluationContext?) -> String - - func getStringValue(key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getStringValue(key: String, defaultValue: String, options: FlagEvaluationOptions) -> String func getStringDetails(key: String, defaultValue: String) -> FlagEvaluationDetails - func getStringDetails(key: String, defaultValue: String, ctx: EvaluationContext?) -> FlagEvaluationDetails - - func getStringDetails(key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getStringDetails(key: String, defaultValue: String, options: FlagEvaluationOptions) -> FlagEvaluationDetails // MARK: Int func getIntegerValue(key: String, defaultValue: Int64) -> Int64 - func getIntegerValue(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> Int64 - - func getIntegerValue(key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getIntegerValue(key: String, defaultValue: Int64, options: FlagEvaluationOptions) -> Int64 func getIntegerDetails(key: String, defaultValue: Int64) -> FlagEvaluationDetails - func getIntegerDetails(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> FlagEvaluationDetails - - func getIntegerDetails(key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getIntegerDetails(key: String, defaultValue: Int64, options: FlagEvaluationOptions) -> FlagEvaluationDetails // MARK: Double func getDoubleValue(key: String, defaultValue: Double) -> Double - func getDoubleValue(key: String, defaultValue: Double, ctx: EvaluationContext?) -> Double - - func getDoubleValue(key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getDoubleValue(key: String, defaultValue: Double, options: FlagEvaluationOptions) -> Double func getDoubleDetails(key: String, defaultValue: Double) -> FlagEvaluationDetails - func getDoubleDetails(key: String, defaultValue: Double, ctx: EvaluationContext?) -> FlagEvaluationDetails - - func getDoubleDetails(key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getDoubleDetails(key: String, defaultValue: Double, options: FlagEvaluationOptions) -> FlagEvaluationDetails // MARK: Object func getObjectValue(key: String, defaultValue: Value) -> Value - func getObjectValue(key: String, defaultValue: Value, ctx: EvaluationContext?) -> Value - - func getObjectValue(key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getObjectValue(key: String, defaultValue: Value, options: FlagEvaluationOptions) -> Value func getObjectDetails(key: String, defaultValue: Value) -> FlagEvaluationDetails - func getObjectDetails(key: String, defaultValue: Value, ctx: EvaluationContext?) -> FlagEvaluationDetails - - func getObjectDetails(key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions) + func getObjectDetails(key: String, defaultValue: Value, options: FlagEvaluationOptions) -> FlagEvaluationDetails } diff --git a/Sources/OpenFeature/Hook.swift b/Sources/OpenFeature/Hook.swift index 40a1dce..2c2dbb6 100644 --- a/Sources/OpenFeature/Hook.swift +++ b/Sources/OpenFeature/Hook.swift @@ -3,7 +3,7 @@ import Foundation public protocol Hook { associatedtype HookValue: Equatable - func before(ctx: HookContext, hints: [String: Any]) -> EvaluationContext? + func before(ctx: HookContext, hints: [String: Any]) func after(ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]) @@ -15,8 +15,7 @@ public protocol Hook { } extension Hook { - func before(ctx: HookContext, hints: [String: Any]) -> EvaluationContext? { - return nil + func before(ctx: HookContext, hints: [String: Any]) { } func after(ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]) { diff --git a/Sources/OpenFeature/HookSupport.swift b/Sources/OpenFeature/HookSupport.swift index 89c461b..28f26c4 100644 --- a/Sources/OpenFeature/HookSupport.swift +++ b/Sources/OpenFeature/HookSupport.swift @@ -109,43 +109,33 @@ class HookSupport { } } - func beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: [AnyHook], hints: [String: Any]) - -> EvaluationContext - { - let result = - hooks + func beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: [AnyHook], hints: [String: Any]) { + hooks .reversed() .filter { hook in hook.supportsFlagValueType(flagValueType: flagValueType) } - .compactMap { hook in + .forEach { hook in switch hook { case .boolean(let booleanHook): if let booleanCtx = hookCtx as? HookContext { - return booleanHook.before(ctx: booleanCtx, hints: hints) + booleanHook.before(ctx: booleanCtx, hints: hints) } case .integer(let integerHook): if let integerCtx = hookCtx as? HookContext { - return integerHook.before(ctx: integerCtx, hints: hints) + integerHook.before(ctx: integerCtx, hints: hints) } case .double(let doubleHook): if let doubleCtx = hookCtx as? HookContext { - return doubleHook.before(ctx: doubleCtx, hints: hints) + doubleHook.before(ctx: doubleCtx, hints: hints) } case .string(let stringHook): if let stringCtx = hookCtx as? HookContext { - return stringHook.before(ctx: stringCtx, hints: hints) + stringHook.before(ctx: stringCtx, hints: hints) } case .object(let objectHook): if let objectCtx = hookCtx as? HookContext { - return objectHook.before(ctx: objectCtx, hints: hints) + objectHook.before(ctx: objectCtx, hints: hints) } } - - return nil } - - return hookCtx.ctx.merge( - overridingContext: result.reduce(hookCtx.ctx) { acc, cur in - acc.merge(overridingContext: cur) - }) } } diff --git a/Sources/OpenFeature/MutableContext.swift b/Sources/OpenFeature/MutableContext.swift index 437c369..c72849d 100644 --- a/Sources/OpenFeature/MutableContext.swift +++ b/Sources/OpenFeature/MutableContext.swift @@ -21,20 +21,6 @@ public class MutableContext: EvaluationContext { self.targetingKey = targetingKey } - public func merge(overridingContext: EvaluationContext) -> EvaluationContext { - let merged = self.asMap().merging(overridingContext.asMap()) { _, new in new } - let mergedContext = MutableContext(attributes: merged) - - if !self.targetingKey.isEmpty { - mergedContext.setTargetingKey(targetingKey: self.targetingKey) - } - if !overridingContext.getTargetingKey().trimmingCharacters(in: .whitespaces).isEmpty { - mergedContext.setTargetingKey(targetingKey: overridingContext.getTargetingKey()) - } - - return mergedContext - } - public func keySet() -> Set { return structure.keySet() } diff --git a/Sources/OpenFeature/NoOpProvider.swift b/Sources/OpenFeature/NoOpProvider.swift index aa4c223..8e9fe58 100644 --- a/Sources/OpenFeature/NoOpProvider.swift +++ b/Sources/OpenFeature/NoOpProvider.swift @@ -6,35 +6,43 @@ class NoOpProvider: FeatureProvider { var metadata: Metadata = NoOpMetadata(name: "No-op provider") var hooks: [AnyHook] = [] - func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws -> ProviderEvaluation< + func onContextSet(oldContext: EvaluationContext, newContext: EvaluationContext) { + // no-op + } + + func initialize(initialContext: EvaluationContext) { + // no-op + } + + func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> ProviderEvaluation< Bool > { return ProviderEvaluation( value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue) } - func getStringEvaluation(key: String, defaultValue: String, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getStringEvaluation(key: String, defaultValue: String) throws -> ProviderEvaluation< String > { return ProviderEvaluation( value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue) } - func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> ProviderEvaluation< Int64 > { return ProviderEvaluation( value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue) } - func getDoubleEvaluation(key: String, defaultValue: Double, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getDoubleEvaluation(key: String, defaultValue: Double) throws -> ProviderEvaluation< Double > { return ProviderEvaluation( value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue) } - func getObjectEvaluation(key: String, defaultValue: Value, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getObjectEvaluation(key: String, defaultValue: Value) throws -> ProviderEvaluation< Value > { return ProviderEvaluation( diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index 7a76894..3c741b7 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -3,45 +3,52 @@ 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 { - // TODO: We use DispatchQueue here instead of being an actor to not lock into new versions of Swift - private let contextQueue = DispatchQueue(label: "dev.openfeature.api.context") - private let providerQueue = DispatchQueue(label: "dev.openfeature.api.provider") - private let hookQueue = DispatchQueue(label: "dev.openfeature.api.hook") - private var _provider: FeatureProvider? - public var provider: FeatureProvider? { - get { - return self._provider - } - set { - self.providerQueue.sync { - self._provider = newValue - } - } + private var _evaluationContext: EvaluationContext = MutableContext() + private(set) var hooks: [AnyHook] = [] + + /// The ``OpenFeatureAPI`` singleton + static public let shared = OpenFeatureAPI() + + public init() { } - private var _evaluationContext: EvaluationContext? - public var evaluationContext: EvaluationContext? { - get { - return self._evaluationContext - } - set { - self.contextQueue.sync { - self._evaluationContext = newValue - } + public func setProvider(provider: FeatureProvider) async { + await self.setProvider(provider: provider, initialContext: nil) + } + + public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) async { + await provider.initialize(initialContext: initialContext ?? self._evaluationContext) + self._provider = provider + guard let newEvaluationContext = initialContext else { + return } + self._evaluationContext = newEvaluationContext } - private(set) var hooks: [AnyHook] = [] + public func getProvider() -> FeatureProvider? { + return self._provider + } - /// The ``OpenFeatureAPI`` singleton - static public let shared = OpenFeatureAPI() + public func clearProvider() { + self._provider = nil + } - public init() { + public func setEvaluationContext(evaluationContext: EvaluationContext) async { + await getProvider()?.onContextSet(oldContext: self._evaluationContext, newContext: evaluationContext) + // A provider evaluation reading the global ctx at this point would fail due to stale cache. + // To prevent this, the provider should internally manage the ctx to use on each evaluation, and + // make sure it's aligned with the values in the cache at all times. If no guarantees are offered by + // the provider, the application can expect STALE resolves while setting a new global ctx + self._evaluationContext = evaluationContext + } + + public func getEvaluationContext() -> EvaluationContext? { + return self._evaluationContext } public func getProviderMetadata() -> Metadata? { - return self.provider?.metadata + return self.getProvider()?.metadata } public func getClient() -> Client { @@ -53,14 +60,10 @@ public class OpenFeatureAPI { } public func addHooks(hooks: AnyHook...) { - hookQueue.sync { - self.hooks.append(contentsOf: hooks) - } + self.hooks.append(contentsOf: hooks) } public func clearHooks() { - hookQueue.sync { - self.hooks.removeAll() - } + self.hooks.removeAll() } } diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift index aaa8e17..f51e998 100644 --- a/Sources/OpenFeature/OpenFeatureClient.swift +++ b/Sources/OpenFeature/OpenFeatureClient.swift @@ -20,18 +20,6 @@ public class OpenFeatureClient: Client { return _hooks } - private var _evaluationContext: EvaluationContext? - public var evaluationContext: EvaluationContext? { - get { - return _evaluationContext - } - set { - contextQueue.sync { - self._evaluationContext = newValue - } - } - } - private var hookSupport = HookSupport() private var logger = Logger() @@ -55,35 +43,25 @@ extension OpenFeatureClient { return getBooleanDetails(key: key, defaultValue: defaultValue).value } - public func getBooleanValue(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> Bool { - return getBooleanDetails(key: key, defaultValue: defaultValue, ctx: ctx).value - } - public func getBooleanValue( - key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Bool, options: FlagEvaluationOptions ) -> Bool { - return getBooleanDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: options).value + return getBooleanDetails(key: key, defaultValue: defaultValue, options: options).value } public func getBooleanDetails(key: String, defaultValue: Bool) -> FlagEvaluationDetails { - return getBooleanDetails(key: key, defaultValue: defaultValue, ctx: nil) - } - - public func getBooleanDetails(key: String, defaultValue: Bool, ctx: EvaluationContext?) -> FlagEvaluationDetails< - Bool - > { - return getBooleanDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: FlagEvaluationOptions()) + return getBooleanDetails(key: key, defaultValue: defaultValue, options: FlagEvaluationOptions()) } public func getBooleanDetails( - key: String, defaultValue: Bool, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Bool, options: FlagEvaluationOptions ) -> FlagEvaluationDetails { return evaluateFlag( - flagValueType: .boolean, key: key, defaultValue: defaultValue, ctx: ctx, options: options + flagValueType: .boolean, key: key, defaultValue: defaultValue, options: options ) } } @@ -94,35 +72,25 @@ extension OpenFeatureClient { return getStringDetails(key: key, defaultValue: defaultValue).value } - public func getStringValue(key: String, defaultValue: String, ctx: EvaluationContext?) -> String { - return getStringDetails(key: key, defaultValue: defaultValue, ctx: ctx).value - } - public func getStringValue( - key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: String, options: FlagEvaluationOptions ) -> String { - return getStringDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: options).value + return getStringDetails(key: key, defaultValue: defaultValue, options: options).value } public func getStringDetails(key: String, defaultValue: String) -> FlagEvaluationDetails { - return getStringDetails(key: key, defaultValue: defaultValue, ctx: nil) - } - - public func getStringDetails(key: String, defaultValue: String, ctx: EvaluationContext?) -> FlagEvaluationDetails< - String - > { - return getStringDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: FlagEvaluationOptions()) + return getStringDetails(key: key, defaultValue: defaultValue, options: FlagEvaluationOptions()) } public func getStringDetails( - key: String, defaultValue: String, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: String, options: FlagEvaluationOptions ) -> FlagEvaluationDetails { return evaluateFlag( - flagValueType: .string, key: key, defaultValue: defaultValue, ctx: ctx, options: options) + flagValueType: .string, key: key, defaultValue: defaultValue, options: options) } } @@ -132,35 +100,25 @@ extension OpenFeatureClient { return getIntegerDetails(key: key, defaultValue: defaultValue).value } - public func getIntegerValue(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> Int64 { - return getIntegerDetails(key: key, defaultValue: defaultValue, ctx: ctx).value - } - public func getIntegerValue( - key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Int64, options: FlagEvaluationOptions ) -> Int64 { - return getIntegerDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: options).value + return getIntegerDetails(key: key, defaultValue: defaultValue, options: options).value } public func getIntegerDetails(key: String, defaultValue: Int64) -> FlagEvaluationDetails { - return getIntegerDetails(key: key, defaultValue: defaultValue, ctx: nil) - } - - public func getIntegerDetails(key: String, defaultValue: Int64, ctx: EvaluationContext?) -> FlagEvaluationDetails< - Int64 - > { - return getIntegerDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: FlagEvaluationOptions()) + return getIntegerDetails(key: key, defaultValue: defaultValue, options: FlagEvaluationOptions()) } public func getIntegerDetails( - key: String, defaultValue: Int64, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Int64, options: FlagEvaluationOptions ) -> FlagEvaluationDetails { return evaluateFlag( - flagValueType: .integer, key: key, defaultValue: defaultValue, ctx: ctx, options: options + flagValueType: .integer, key: key, defaultValue: defaultValue, options: options ) } } @@ -171,35 +129,25 @@ extension OpenFeatureClient { return getDoubleDetails(key: key, defaultValue: defaultValue).value } - public func getDoubleValue(key: String, defaultValue: Double, ctx: EvaluationContext?) -> Double { - return getDoubleDetails(key: key, defaultValue: defaultValue, ctx: ctx).value - } - public func getDoubleValue( - key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Double, options: FlagEvaluationOptions ) -> Double { - return getDoubleDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: options).value + return getDoubleDetails(key: key, defaultValue: defaultValue, options: options).value } public func getDoubleDetails(key: String, defaultValue: Double) -> FlagEvaluationDetails { - return getDoubleDetails(key: key, defaultValue: defaultValue, ctx: nil) - } - - public func getDoubleDetails(key: String, defaultValue: Double, ctx: EvaluationContext?) -> FlagEvaluationDetails< - Double - > { - return getDoubleDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: FlagEvaluationOptions()) + return getDoubleDetails(key: key, defaultValue: defaultValue, options: FlagEvaluationOptions()) } public func getDoubleDetails( - key: String, defaultValue: Double, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Double, options: FlagEvaluationOptions ) -> FlagEvaluationDetails { return evaluateFlag( - flagValueType: .double, key: key, defaultValue: defaultValue, ctx: ctx, options: options) + flagValueType: .double, key: key, defaultValue: defaultValue, options: options) } } @@ -209,35 +157,25 @@ extension OpenFeatureClient { return getObjectDetails(key: key, defaultValue: defaultValue).value } - public func getObjectValue(key: String, defaultValue: Value, ctx: EvaluationContext?) -> Value { - return getObjectDetails(key: key, defaultValue: defaultValue, ctx: ctx).value - } - public func getObjectValue( - key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Value, options: FlagEvaluationOptions ) -> Value { - return getObjectDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: options).value + return getObjectDetails(key: key, defaultValue: defaultValue, options: options).value } public func getObjectDetails(key: String, defaultValue: Value) -> FlagEvaluationDetails { - return getObjectDetails(key: key, defaultValue: defaultValue, ctx: nil) - } - - public func getObjectDetails(key: String, defaultValue: Value, ctx: EvaluationContext?) -> FlagEvaluationDetails< - Value - > { - return getObjectDetails(key: key, defaultValue: defaultValue, ctx: ctx, options: FlagEvaluationOptions()) + return getObjectDetails(key: key, defaultValue: defaultValue, options: FlagEvaluationOptions()) } public func getObjectDetails( - key: String, defaultValue: Value, ctx: EvaluationContext?, options: FlagEvaluationOptions + key: String, defaultValue: Value, options: FlagEvaluationOptions ) -> FlagEvaluationDetails { return evaluateFlag( - flagValueType: .object, key: key, defaultValue: defaultValue, ctx: ctx, options: options) + flagValueType: .object, key: key, defaultValue: defaultValue, options: options) } } @@ -252,39 +190,31 @@ extension OpenFeatureClient { flagValueType: FlagValueType, key: String, defaultValue: T, - ctx: EvaluationContext?, options: FlagEvaluationOptions? ) -> FlagEvaluationDetails { let options = options ?? FlagEvaluationOptions(hooks: [], hookHints: [:]) let hints = options.hookHints + let context = openFeatureApi.getEvaluationContext() ?? MutableContext() - let ctx = ctx ?? MutableContext() var details = FlagEvaluationDetails(flagKey: key, value: defaultValue) - let provider = openFeatureApi.provider ?? NoOpProvider() + let provider = openFeatureApi.getProvider() ?? NoOpProvider() let mergedHooks = provider.hooks + options.hooks + hooks + openFeatureApi.hooks let hookCtx = HookContext( flagKey: key, type: flagValueType, defaultValue: defaultValue, - ctx: ctx, + ctx: context, clientMetadata: self.metadata, providerMetadata: provider.metadata) do { - let apiContext = openFeatureApi.evaluationContext ?? MutableContext() - let clientContext = self.evaluationContext ?? MutableContext() - - let ctxFromHook = hookSupport.beforeHooks( - flagValueType: flagValueType, hookCtx: hookCtx, hooks: mergedHooks, hints: hints) - let invocationCtx = ctx.merge(overridingContext: ctxFromHook) - let mergedCtx = apiContext.merge(overridingContext: clientContext.merge(overridingContext: invocationCtx)) + hookSupport.beforeHooks(flagValueType: flagValueType, hookCtx: hookCtx, hooks: mergedHooks, hints: hints) let providerEval = try createProviderEvaluation( flagValueType: flagValueType, key: key, defaultValue: defaultValue, - provider: provider, - invocationContext: mergedCtx) + provider: provider) let evalDetails = FlagEvaluationDetails.from(providerEval: providerEval, flagKey: key) details = evalDetails @@ -318,8 +248,7 @@ extension OpenFeatureClient { flagValueType: FlagValueType, key: String, defaultValue: V, - provider: FeatureProvider, - invocationContext: EvaluationContext + provider: FeatureProvider ) throws -> ProviderEvaluation { switch flagValueType { case .boolean: @@ -329,8 +258,7 @@ extension OpenFeatureClient { if let evaluation = try provider.getBooleanEvaluation( key: key, - defaultValue: defaultValue, - ctx: invocationContext) as? ProviderEvaluation + defaultValue: defaultValue) as? ProviderEvaluation { return evaluation } @@ -341,8 +269,7 @@ extension OpenFeatureClient { if let evaluation = try provider.getStringEvaluation( key: key, - defaultValue: defaultValue, - ctx: invocationContext) as? ProviderEvaluation + defaultValue: defaultValue) as? ProviderEvaluation { return evaluation } @@ -353,8 +280,7 @@ extension OpenFeatureClient { if let evaluation = try provider.getIntegerEvaluation( key: key, - defaultValue: defaultValue, - ctx: invocationContext) as? ProviderEvaluation + defaultValue: defaultValue) as? ProviderEvaluation { return evaluation } @@ -365,8 +291,7 @@ extension OpenFeatureClient { if let evaluation = try provider.getDoubleEvaluation( key: key, - defaultValue: defaultValue, - ctx: invocationContext) as? ProviderEvaluation + defaultValue: defaultValue) as? ProviderEvaluation { return evaluation } @@ -377,8 +302,7 @@ extension OpenFeatureClient { if let evaluation = try provider.getObjectEvaluation( key: key, - defaultValue: defaultValue, - ctx: invocationContext) as? ProviderEvaluation + defaultValue: defaultValue) as? ProviderEvaluation { return evaluation } diff --git a/Sources/OpenFeature/Reason.swift b/Sources/OpenFeature/Reason.swift index e722b7b..5da58fb 100644 --- a/Sources/OpenFeature/Reason.swift +++ b/Sources/OpenFeature/Reason.swift @@ -11,6 +11,8 @@ public enum Reason: String { case defaultReason /// The reason for the resolved value could not be determined. case unknown + /// The resolved value is non-authoritative or possible out of date + case stale /// The resolved value was the result of an error. case error } diff --git a/Sources/OpenFeature/exceptions/ErrorCode.swift b/Sources/OpenFeature/exceptions/ErrorCode.swift index 23e2a2b..0b7b033 100644 --- a/Sources/OpenFeature/exceptions/ErrorCode.swift +++ b/Sources/OpenFeature/exceptions/ErrorCode.swift @@ -1,7 +1,7 @@ import Foundation public enum ErrorCode: Int { - case providerNotReady = 1 + case providerNotReady case flagNotFound case parseError case typeMismatch diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index b1eeb6d..61e400e 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -4,23 +4,23 @@ import XCTest final class DeveloperExperienceTests: XCTestCase { func testNoProviderSet() { - OpenFeatureAPI.shared.provider = nil + OpenFeatureAPI.shared.clearProvider() let client = OpenFeatureAPI.shared.getClient() let flagValue = client.getStringValue(key: "test", defaultValue: "no-op") XCTAssertEqual(flagValue, "no-op") } - func testSimpleBooleanFlag() { - OpenFeatureAPI.shared.provider = NoOpProvider() + func testSimpleBooleanFlag() async { + await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let flagValue = client.getBooleanValue(key: "test", defaultValue: false) XCTAssertFalse(flagValue) } - func testClientHooks() { - OpenFeatureAPI.shared.provider = NoOpProvider() + func testClientHooks() async { + await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let hook = BooleanHookMock() @@ -30,36 +30,19 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testEvalHooks() { - OpenFeatureAPI.shared.provider = NoOpProvider() + func testEvalHooks() async { + await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let hook = BooleanHookMock() let options = FlagEvaluationOptions(hooks: [.boolean(hook)]) - _ = client.getBooleanValue(key: "test", defaultValue: false, ctx: nil, options: options) + _ = client.getBooleanValue(key: "test", defaultValue: false, options: options) XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testProvidingContext() { - let provider = NoOpProviderMock() - OpenFeatureAPI.shared.provider = provider - let client = OpenFeatureAPI.shared.getClient() - - let ctx = MutableContext() - .add(key: "int-val", value: .integer(3)) - .add(key: "double-val", value: .double(4.0)) - .add(key: "bool-val", value: .boolean(false)) - .add(key: "str-val", value: .string("test")) - .add(key: "value-val", value: .list([.integer(2), .integer(4)])) - - _ = client.getBooleanValue(key: "test", defaultValue: false, ctx: ctx) - - XCTAssertEqual(ctx.asMap(), provider.ctxWhenCalled?.asMap()) - } - - func testBrokenProvider() { - OpenFeatureAPI.shared.provider = AlwaysBrokenProvider() + func testBrokenProvider() async { + await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) let client = OpenFeatureAPI.shared.getClient() let details = client.getBooleanDetails(key: "test", defaultValue: false) @@ -69,17 +52,3 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(details.reason, Reason.error.rawValue) } } - -extension DeveloperExperienceTests { - class NoOpProviderMock: NoOpProvider { - var ctxWhenCalled: EvaluationContext? - - override func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws - -> ProviderEvaluation - { - self.ctxWhenCalled = ctx - - return try super.getBooleanEvaluation(key: key, defaultValue: defaultValue, ctx: ctx) - } - } -} diff --git a/Tests/OpenFeatureTests/EvalContextTests.swift b/Tests/OpenFeatureTests/EvalContextTests.swift index 905cee5..00a4242 100644 --- a/Tests/OpenFeatureTests/EvalContextTests.swift +++ b/Tests/OpenFeatureTests/EvalContextTests.swift @@ -112,24 +112,6 @@ final class EvalContextTests: XCTestCase { XCTAssertNil(ctx.getValue(key: "null")?.asString()) } - func testContextCanMergeTargetingKey() { - let key1 = "key1" - let ctx1 = MutableContext(targetingKey: key1) - let ctx2 = MutableContext() - - let merged = ctx1.merge(overridingContext: ctx2) - XCTAssertEqual(merged.getTargetingKey(), key1) - - let key2 = "key2" - ctx2.setTargetingKey(targetingKey: "key2") - let merged2 = ctx1.merge(overridingContext: ctx2) - XCTAssertEqual(merged2.getTargetingKey(), key2) - - ctx2.setTargetingKey(targetingKey: " ") - let merged3 = ctx1.merge(overridingContext: ctx2) - XCTAssertEqual(merged3.getTargetingKey(), key1) - } - func testContextConvertsToObjectMap() { let key1 = "key1" let date = Date.now diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift index d1ed25e..516b913 100644 --- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift +++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift @@ -8,15 +8,15 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertTrue(OpenFeatureAPI.shared === OpenFeatureAPI.shared) } - func testApiSetsProvider() { + func testApiSetsProvider() async { let provider = NoOpProvider() - OpenFeatureAPI.shared.provider = provider + await OpenFeatureAPI.shared.setProvider(provider: provider) - XCTAssertTrue((OpenFeatureAPI.shared.provider as? NoOpProvider) === provider) + XCTAssertTrue((OpenFeatureAPI.shared.getProvider() as? NoOpProvider) === provider) } - func testProviderMetadata() { - OpenFeatureAPI.shared.provider = DoSomethingProvider() + func testProviderMetadata() async { + await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) XCTAssertEqual(OpenFeatureAPI.shared.getProviderMetadata()?.name, DoSomethingProvider.name) } @@ -51,88 +51,88 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(client.hooks.count, 2) } - func testSimpleFlagEvaluation() { - OpenFeatureAPI.shared.provider = DoSomethingProvider() + func testSimpleFlagEvaluation() async { + await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) let client = OpenFeatureAPI.shared.getClient() let key = "key" XCTAssertEqual(client.getBooleanValue(key: key, defaultValue: false), true) - XCTAssertEqual(client.getBooleanValue(key: key, defaultValue: false, ctx: MutableContext()), true) + XCTAssertEqual(client.getBooleanValue(key: key, defaultValue: false), true) XCTAssertEqual( client.getBooleanValue( - key: key, defaultValue: false, ctx: MutableContext(), options: FlagEvaluationOptions()), true) + key: key, defaultValue: false, options: FlagEvaluationOptions()), true) XCTAssertEqual(client.getStringValue(key: key, defaultValue: "test"), "tset") - XCTAssertEqual(client.getStringValue(key: key, defaultValue: "test", ctx: MutableContext()), "tset") + XCTAssertEqual(client.getStringValue(key: key, defaultValue: "test"), "tset") XCTAssertEqual( client.getStringValue( - key: key, defaultValue: "test", ctx: MutableContext(), options: FlagEvaluationOptions()), "tset") + key: key, defaultValue: "test", options: FlagEvaluationOptions()), "tset") XCTAssertEqual(client.getIntegerValue(key: key, defaultValue: 4), 400) - XCTAssertEqual(client.getIntegerValue(key: key, defaultValue: 4, ctx: MutableContext()), 400) + XCTAssertEqual(client.getIntegerValue(key: key, defaultValue: 4), 400) XCTAssertEqual( - client.getIntegerValue(key: key, defaultValue: 4, ctx: MutableContext(), options: FlagEvaluationOptions()), + client.getIntegerValue(key: key, defaultValue: 4, options: FlagEvaluationOptions()), 400) XCTAssertEqual(client.getDoubleValue(key: key, defaultValue: 0.4), 40.0) - XCTAssertEqual(client.getDoubleValue(key: key, defaultValue: 0.4, ctx: MutableContext()), 40.0) + XCTAssertEqual(client.getDoubleValue(key: key, defaultValue: 0.4), 40.0) XCTAssertEqual( - client.getDoubleValue(key: key, defaultValue: 0.4, ctx: MutableContext(), options: FlagEvaluationOptions()), + client.getDoubleValue(key: key, defaultValue: 0.4, options: FlagEvaluationOptions()), 40.0) XCTAssertEqual(client.getObjectValue(key: key, defaultValue: .structure([:])), .null) - XCTAssertEqual(client.getObjectValue(key: key, defaultValue: .structure([:]), ctx: MutableContext()), .null) + XCTAssertEqual(client.getObjectValue(key: key, defaultValue: .structure([:])), .null) XCTAssertEqual( client.getObjectValue( - key: key, defaultValue: .structure([:]), ctx: MutableContext(), options: FlagEvaluationOptions()), .null + key: key, defaultValue: .structure([:]), options: FlagEvaluationOptions()), .null ) } - func testDetailedFlagEvaluation() { - OpenFeatureAPI.shared.provider = DoSomethingProvider() + func testDetailedFlagEvaluation() async { + await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) let client = OpenFeatureAPI.shared.getClient() let key = "key" let booleanDetails = FlagEvaluationDetails(flagKey: key, value: true, variant: nil) XCTAssertEqual(client.getBooleanDetails(key: key, defaultValue: false), booleanDetails) - XCTAssertEqual(client.getBooleanDetails(key: key, defaultValue: false, ctx: MutableContext()), booleanDetails) + XCTAssertEqual(client.getBooleanDetails(key: key, defaultValue: false), booleanDetails) XCTAssertEqual( client.getBooleanDetails( - key: key, defaultValue: false, ctx: MutableContext(), options: FlagEvaluationOptions()), booleanDetails) + key: key, defaultValue: false, options: FlagEvaluationOptions()), booleanDetails) let stringDetails = FlagEvaluationDetails(flagKey: key, value: "tset", variant: nil) XCTAssertEqual(client.getStringDetails(key: key, defaultValue: "test"), stringDetails) - XCTAssertEqual(client.getStringDetails(key: key, defaultValue: "test", ctx: MutableContext()), stringDetails) + XCTAssertEqual(client.getStringDetails(key: key, defaultValue: "test"), stringDetails) XCTAssertEqual( client.getStringDetails( - key: key, defaultValue: "test", ctx: MutableContext(), options: FlagEvaluationOptions()), stringDetails) + key: key, defaultValue: "test", options: FlagEvaluationOptions()), stringDetails) let integerDetails = FlagEvaluationDetails(flagKey: key, value: Int64(400), variant: nil) XCTAssertEqual(client.getIntegerDetails(key: key, defaultValue: 4), integerDetails) - XCTAssertEqual(client.getIntegerDetails(key: key, defaultValue: 4, ctx: MutableContext()), integerDetails) + XCTAssertEqual(client.getIntegerDetails(key: key, defaultValue: 4), integerDetails) XCTAssertEqual( client.getIntegerDetails( - key: key, defaultValue: 4, ctx: MutableContext(), options: FlagEvaluationOptions()), integerDetails) + key: key, defaultValue: 4, options: FlagEvaluationOptions()), integerDetails) let doubleDetails = FlagEvaluationDetails(flagKey: key, value: 40.0, variant: nil) XCTAssertEqual(client.getDoubleDetails(key: key, defaultValue: 0.4), doubleDetails) - XCTAssertEqual(client.getDoubleDetails(key: key, defaultValue: 0.4, ctx: MutableContext()), doubleDetails) + XCTAssertEqual(client.getDoubleDetails(key: key, defaultValue: 0.4), doubleDetails) XCTAssertEqual( client.getDoubleDetails( - key: key, defaultValue: 0.4, ctx: MutableContext(), options: FlagEvaluationOptions()), doubleDetails) + key: key, defaultValue: 0.4, options: FlagEvaluationOptions()), doubleDetails) let objectDetails = FlagEvaluationDetails(flagKey: key, value: Value.null, variant: nil) XCTAssertEqual(client.getObjectDetails(key: key, defaultValue: .structure([:])), objectDetails) XCTAssertEqual( - client.getObjectDetails(key: key, defaultValue: .structure([:]), ctx: MutableContext()), objectDetails) + client.getObjectDetails(key: key, defaultValue: .structure([:])), objectDetails) XCTAssertEqual( client.getObjectDetails( - key: key, defaultValue: .structure([:]), ctx: MutableContext(), options: FlagEvaluationOptions()), + key: key, defaultValue: .structure([:]), options: FlagEvaluationOptions()), objectDetails) } - func testHooksAreFired() { - OpenFeatureAPI.shared.provider = NoOpProvider() + func testHooksAreFired() async { + await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let clientHook = BooleanHookMock() @@ -142,15 +142,14 @@ final class FlagEvaluationTests: XCTestCase { _ = client.getBooleanValue( key: "key", defaultValue: false, - ctx: MutableContext(), options: FlagEvaluationOptions(hooks: [.boolean(invocationHook)])) XCTAssertEqual(clientHook.beforeCalled, 1) XCTAssertEqual(invocationHook.beforeCalled, 1) } - func testBrokenProvider() { - OpenFeatureAPI.shared.provider = AlwaysBrokenProvider() + func testBrokenProvider() async { + await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) let client = OpenFeatureAPI.shared.getClient() XCTAssertFalse(client.getBooleanValue(key: "testkey", defaultValue: false)) @@ -168,35 +167,4 @@ final class FlagEvaluationTests: XCTestCase { let client = OpenFeatureAPI.shared.getClient(name: "test", version: nil) XCTAssertEqual(client.metadata.name, "test") } - - func testMultilayerContextMergesCorrectly() { - let provider = DoSomethingProvider() - OpenFeatureAPI.shared.provider = provider - - let apiCtx = MutableContext() - apiCtx.add(key: "common", value: .string("1")) - apiCtx.add(key: "common2", value: .string("1")) - apiCtx.add(key: "api", value: .string("2")) - OpenFeatureAPI.shared.evaluationContext = apiCtx - - var client = OpenFeatureAPI.shared.getClient() - let clientCtx = MutableContext() - clientCtx.add(key: "common", value: .string("3")) - clientCtx.add(key: "common2", value: .string("3")) - clientCtx.add(key: "client", value: .string("4")) - client.evaluationContext = clientCtx - - let invocationCtx = MutableContext() - invocationCtx.add(key: "common", value: .string("5")) - invocationCtx.add(key: "invocation", value: .string("6")) - - _ = client.getBooleanValue(key: "key", defaultValue: false, ctx: invocationCtx) - - let merged = provider.mergedContext - XCTAssertEqual(merged?.getValue(key: "invocation")?.asString(), "6") - XCTAssertEqual(merged?.getValue(key: "common")?.asString(), "5") - XCTAssertEqual(merged?.getValue(key: "client")?.asString(), "4") - XCTAssertEqual(merged?.getValue(key: "common2")?.asString(), "3") - XCTAssertEqual(merged?.getValue(key: "api")?.asString(), "2") - } } diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift index 413afa4..c68f862 100644 --- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift @@ -6,31 +6,39 @@ class AlwaysBrokenProvider: FeatureProvider { var metadata: Metadata = AlwaysBrokenMetadata() var hooks: [AnyHook] = [] - func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: OpenFeature.EvaluationContext) throws + func onContextSet(oldContext: OpenFeature.EvaluationContext, newContext: OpenFeature.EvaluationContext) { + // no-op + } + + func initialize(initialContext: OpenFeature.EvaluationContext) { + // no-op + } + + func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> OpenFeature.ProviderEvaluation { throw OpenFeatureError.flagNotFoundError(key: key) } - func getStringEvaluation(key: String, defaultValue: String, ctx: OpenFeature.EvaluationContext) throws + func getStringEvaluation(key: String, defaultValue: String) throws -> OpenFeature.ProviderEvaluation { throw OpenFeatureError.flagNotFoundError(key: key) } - func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: OpenFeature.EvaluationContext) throws + func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> OpenFeature.ProviderEvaluation { throw OpenFeatureError.flagNotFoundError(key: key) } - func getDoubleEvaluation(key: String, defaultValue: Double, ctx: OpenFeature.EvaluationContext) throws + func getDoubleEvaluation(key: String, defaultValue: Double) throws -> OpenFeature.ProviderEvaluation { throw OpenFeatureError.flagNotFoundError(key: key) } - func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, ctx: OpenFeature.EvaluationContext) throws + func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value) throws -> OpenFeature.ProviderEvaluation { throw OpenFeatureError.flagNotFoundError(key: key) diff --git a/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift b/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift index dacf11f..1d92164 100644 --- a/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift +++ b/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift @@ -20,11 +20,9 @@ class BooleanHookMock: BooleanHook { self.addEval = addEval } - func before(ctx: HookContext, hints: [String: Any]) -> EvaluationContext? { + func before(ctx: HookContext, hints: [String: Any]) { beforeCalled += 1 self.addEval(self.prefix.isEmpty ? "before" : "\(self.prefix) before") - - return nil } func after(ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]) { diff --git a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift index 9a1f150..72d1a3c 100644 --- a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift @@ -3,46 +3,45 @@ import OpenFeature class DoSomethingProvider: FeatureProvider { public static let name = "Something" - private var savedContext: EvaluationContext? - var mergedContext: EvaluationContext? { - savedContext + + func onContextSet(oldContext: OpenFeature.EvaluationContext, newContext: OpenFeature.EvaluationContext) { + // no-op + } + + func initialize(initialContext: OpenFeature.EvaluationContext) { + // no-op } var hooks: [OpenFeature.AnyHook] = [] var metadata: OpenFeature.Metadata = DoMetadata() - func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> ProviderEvaluation< Bool > { - savedContext = ctx return ProviderEvaluation(value: !defaultValue) } - func getStringEvaluation(key: String, defaultValue: String, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getStringEvaluation(key: String, defaultValue: String) throws -> ProviderEvaluation< String > { - savedContext = ctx return ProviderEvaluation(value: String(defaultValue.reversed())) } - func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> ProviderEvaluation< Int64 > { - savedContext = ctx return ProviderEvaluation(value: defaultValue * 100) } - func getDoubleEvaluation(key: String, defaultValue: Double, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getDoubleEvaluation(key: String, defaultValue: Double) throws -> ProviderEvaluation< Double > { - savedContext = ctx return ProviderEvaluation(value: defaultValue * 100) } - func getObjectEvaluation(key: String, defaultValue: Value, ctx: EvaluationContext) throws -> ProviderEvaluation< + func getObjectEvaluation(key: String, defaultValue: Value) throws -> ProviderEvaluation< Value > { - savedContext = ctx return ProviderEvaluation(value: .null) } diff --git a/Tests/OpenFeatureTests/HookSpecTests.swift b/Tests/OpenFeatureTests/HookSpecTests.swift index 563415f..fdb1094 100644 --- a/Tests/OpenFeatureTests/HookSpecTests.swift +++ b/Tests/OpenFeatureTests/HookSpecTests.swift @@ -5,7 +5,7 @@ import XCTest final class HookSpecTests: XCTestCase { func testNoErrorHookCalled() { - OpenFeatureAPI.shared.provider = NoOpProvider() + OpenFeatureAPI.shared.clearProvider() let client = OpenFeatureAPI.shared.getClient() let hook = BooleanHookMock() @@ -13,7 +13,6 @@ final class HookSpecTests: XCTestCase { _ = client.getBooleanValue( key: "key", defaultValue: false, - ctx: MutableContext(), options: FlagEvaluationOptions(hooks: [.boolean(hook)])) XCTAssertEqual(hook.beforeCalled, 1) @@ -22,16 +21,14 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testErrorHookButNoAfterCalled() { - OpenFeatureAPI.shared.provider = AlwaysBrokenProvider() + func testErrorHookButNoAfterCalled() async { + await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) let client = OpenFeatureAPI.shared.getClient() - let hook = BooleanHookMock() _ = client.getBooleanValue( key: "key", defaultValue: false, - ctx: MutableContext(), options: FlagEvaluationOptions(hooks: [.boolean(hook)])) XCTAssertEqual(hook.beforeCalled, 1) @@ -40,15 +37,16 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testHookEvaluationOrder() { + func testHookEvaluationOrder() async { var evalOrder: [String] = [] let addEval: (String) -> Void = { eval in evalOrder.append(eval) } - OpenFeatureAPI.shared.provider = NoOpProviderMock(hooks: [ + let providerMock = NoOpProviderMock(hooks: [ .boolean(BooleanHookMock(prefix: "provider", addEval: addEval)) ]) + await OpenFeatureAPI.shared.setProvider(provider: providerMock) OpenFeatureAPI.shared.addHooks(hooks: .boolean(BooleanHookMock(prefix: "api", addEval: addEval))) let client = OpenFeatureAPI.shared.getClient() client.addHooks(.boolean(BooleanHookMock(prefix: "client", addEval: addEval))) @@ -56,7 +54,7 @@ final class HookSpecTests: XCTestCase { .boolean(BooleanHookMock(prefix: "invocation", addEval: addEval)) ]) - _ = client.getBooleanValue(key: "key", defaultValue: false, ctx: MutableContext(), options: flagOptions) + _ = client.getBooleanValue(key: "key", defaultValue: false, options: flagOptions) XCTAssertEqual( evalOrder, diff --git a/Tests/OpenFeatureTests/HookSupportTests.swift b/Tests/OpenFeatureTests/HookSupportTests.swift index acb2111..d27f862 100644 --- a/Tests/OpenFeatureTests/HookSupportTests.swift +++ b/Tests/OpenFeatureTests/HookSupportTests.swift @@ -4,31 +4,6 @@ import XCTest @testable import OpenFeature final class HookSupportTests: XCTestCase { - func testShouldMergeEvaluationContextsOnBeforeHooks() { - let metadata = OpenFeatureAPI.shared.getClient().metadata - let baseContext = MutableContext() - baseContext.add(key: "baseKey", value: .string("baseValue")) - - let hook1: AnyHook = .string(StringHookMock(key: "bla", value: "blubber")) - let hook2: AnyHook = .string(StringHookMock(key: "foo", value: "bar")) - - let hookSupport = HookSupport() - let hookContext: HookContext = HookContext( - flagKey: "flagKey", - type: .string, - defaultValue: "defaultValue", - ctx: baseContext, - clientMetadata: metadata, - providerMetadata: NoOpProvider().metadata) - - let result = hookSupport.beforeHooks( - flagValueType: .string, hookCtx: hookContext, hooks: [hook1, hook2], hints: [:]) - - XCTAssertEqual(result.getValue(key: "bla")?.asString(), "blubber") - XCTAssertEqual(result.getValue(key: "foo")?.asString(), "bar") - XCTAssertEqual(result.getValue(key: "baseKey")?.asString(), "baseValue") - } - func testShouldAlwaysCallGenericHook() throws { let metadata = OpenFeatureAPI.shared.getClient().metadata let hook = BooleanHookMock() @@ -43,7 +18,7 @@ final class HookSupportTests: XCTestCase { let hookSupport = HookSupport() - _ = hookSupport.beforeHooks( + hookSupport.beforeHooks( flagValueType: .boolean, hookCtx: hookContext, hooks: [boolHook], diff --git a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift index 05bf15a..30d1050 100644 --- a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift +++ b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift @@ -4,8 +4,8 @@ import XCTest @testable import OpenFeature final class OpenFeatureClientTests: XCTestCase { - func testShouldNowThrowIfHookHasDifferentTypeArgument() { - OpenFeatureAPI.shared.provider = DoSomethingProvider() + func testShouldNowThrowIfHookHasDifferentTypeArgument() async { + await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) OpenFeatureAPI.shared.addHooks(hooks: .boolean(BooleanHookMock())) let client = OpenFeatureAPI.shared.getClient() @@ -14,19 +14,6 @@ final class OpenFeatureClientTests: XCTestCase { XCTAssertEqual(details.value, "tset") } - - func testMergeContexts() { - let targetingKey = "targetingKey" - OpenFeatureAPI.shared.provider = TestProvider(targetingKey: targetingKey) - let ctx = MutableContext(targetingKey: targetingKey) - - var client = OpenFeatureAPI.shared.getClient() - client.evaluationContext = ctx - - let details = client.getBooleanDetails(key: "flag", defaultValue: false) - - XCTAssertEqual(details.value, true) - } } extension OpenFeatureClientTests { @@ -39,43 +26,46 @@ extension OpenFeatureClientTests { class TestProvider: FeatureProvider { var hooks: [OpenFeature.AnyHook] = [] - var metadata: OpenFeature.Metadata = TestMetadata() private var targetingKey: String + func onContextSet(oldContext: OpenFeature.EvaluationContext, newContext: OpenFeature.EvaluationContext) { + // no-op + } + + func initialize(initialContext: OpenFeature.EvaluationContext) { + // no-op + } + init(targetingKey: String) { self.targetingKey = targetingKey } - func getBooleanEvaluation(key: String, defaultValue: Bool, ctx: OpenFeature.EvaluationContext) throws + func getBooleanEvaluation(key: String, defaultValue: Bool) throws -> OpenFeature.ProviderEvaluation { - if ctx.getTargetingKey() == self.targetingKey { - return ProviderEvaluation(value: true) - } else { - return ProviderEvaluation(value: false) - } + return ProviderEvaluation(value: true) } - func getStringEvaluation(key: String, defaultValue: String, ctx: OpenFeature.EvaluationContext) throws + func getStringEvaluation(key: String, defaultValue: String) throws -> OpenFeature.ProviderEvaluation { return ProviderEvaluation(value: "") } - func getIntegerEvaluation(key: String, defaultValue: Int64, ctx: OpenFeature.EvaluationContext) throws + func getIntegerEvaluation(key: String, defaultValue: Int64) throws -> OpenFeature.ProviderEvaluation { return ProviderEvaluation(value: 0) } - func getDoubleEvaluation(key: String, defaultValue: Double, ctx: OpenFeature.EvaluationContext) throws + func getDoubleEvaluation(key: String, defaultValue: Double) throws -> OpenFeature.ProviderEvaluation { return ProviderEvaluation(value: 0.0) } - func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, ctx: OpenFeature.EvaluationContext) + func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value) throws -> OpenFeature.ProviderEvaluation { return ProviderEvaluation(value: .null) diff --git a/Tests/OpenFeatureTests/ProviderSpecTests.swift b/Tests/OpenFeatureTests/ProviderSpecTests.swift index 305b7ff..bb7bb7c 100644 --- a/Tests/OpenFeatureTests/ProviderSpecTests.swift +++ b/Tests/OpenFeatureTests/ProviderSpecTests.swift @@ -7,52 +7,52 @@ final class ProviderSpecTests: XCTestCase { func testFlagValueSet() throws { let provider = NoOpProvider() - let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false, ctx: MutableContext()) + let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false) XCTAssertNotNil(boolResult.value) - let stringResult = try provider.getStringEvaluation(key: "key", defaultValue: "test", ctx: MutableContext()) + let stringResult = try provider.getStringEvaluation(key: "key", defaultValue: "test") XCTAssertNotNil(stringResult.value) - let intResult = try provider.getIntegerEvaluation(key: "key", defaultValue: 4, ctx: MutableContext()) + let intResult = try provider.getIntegerEvaluation(key: "key", defaultValue: 4) XCTAssertNotNil(intResult.value) - let doubleResult = try provider.getDoubleEvaluation(key: "key", defaultValue: 0.4, ctx: MutableContext()) + let doubleResult = try provider.getDoubleEvaluation(key: "key", defaultValue: 0.4) XCTAssertNotNil(doubleResult.value) - let objectResult = try provider.getObjectEvaluation(key: "key", defaultValue: .null, ctx: MutableContext()) + let objectResult = try provider.getObjectEvaluation(key: "key", defaultValue: .null) XCTAssertNotNil(objectResult.value) } func testHasReason() throws { let provider = NoOpProvider() - let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false, ctx: MutableContext()) + let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false) XCTAssertEqual(boolResult.reason, Reason.defaultReason.rawValue) } func testNoErrorCodeByDefault() throws { let provider = NoOpProvider() - let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false, ctx: MutableContext()) + let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false) XCTAssertNil(boolResult.errorCode) } func testVariantIsSet() throws { let provider = NoOpProvider() - let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false, ctx: MutableContext()) + let boolResult = try provider.getBooleanEvaluation(key: "key", defaultValue: false) XCTAssertNotNil(boolResult.variant) - let stringResult = try provider.getStringEvaluation(key: "key", defaultValue: "test", ctx: MutableContext()) + let stringResult = try provider.getStringEvaluation(key: "key", defaultValue: "test") XCTAssertNotNil(stringResult.variant) - let intResult = try provider.getIntegerEvaluation(key: "key", defaultValue: 4, ctx: MutableContext()) + let intResult = try provider.getIntegerEvaluation(key: "key", defaultValue: 4) XCTAssertNotNil(intResult.variant) - let doubleResult = try provider.getDoubleEvaluation(key: "key", defaultValue: 0.4, ctx: MutableContext()) + let doubleResult = try provider.getDoubleEvaluation(key: "key", defaultValue: 0.4) XCTAssertNotNil(doubleResult.variant) - let objectResult = try provider.getObjectEvaluation(key: "key", defaultValue: .null, ctx: MutableContext()) + let objectResult = try provider.getObjectEvaluation(key: "key", defaultValue: .null) XCTAssertNotNil(objectResult.variant) } }