From e2d33ad631a7cfa2e8d21de003011d67a728710b Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 25 Aug 2025 11:45:13 -0700 Subject: [PATCH 01/15] Add MultiProvider Signed-off-by: jescriba --- .../MultiProvider/FirstMatchStrategy.swift | 34 ++++++ .../FirstSuccessfulStrategy.swift | 26 +++++ .../MultiProvider/MultiProvider.swift | 108 ++++++++++++++++++ .../Provider/MultiProvider/Strategy.swift | 19 +++ 4 files changed, 187 insertions(+) create mode 100644 Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift create mode 100644 Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift create mode 100644 Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift create mode 100644 Sources/OpenFeature/Provider/MultiProvider/Strategy.swift diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift new file mode 100644 index 0000000..2e51ac4 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -0,0 +1,34 @@ +/// FirstMatchStrategy is a strategy that evaluates a feature flag across multiple providers +/// and returns the first result. Skips providers that indicate they had no value due to flag not found. +/// If any provider returns an error result other than flag not found, the error is returned. +final public class FirstMatchStrategy: Strategy { + + public init() {} + + public func evaluate( + providers: [FeatureProvider], + key: String, + defaultValue: T, + evaluationContext: EvaluationContext?, + flagEvaluation: FlagEvaluation + ) throws -> ProviderEvaluation where T: AllowedFlagValueType { + for provider in providers { + do { + let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) + if eval.errorCode != ErrorCode.flagNotFound { + return eval + } + } catch OpenFeatureError.flagNotFoundError { + continue + } catch { + throw error + } + } + + return ProviderEvaluation( + value: defaultValue, + reason: Reason.defaultReason.rawValue, + errorCode: ErrorCode.flagNotFound + ) + } +} diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift new file mode 100644 index 0000000..003a8a8 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -0,0 +1,26 @@ +/// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers +/// and returns the first result. Similar to `FirstMatchStrategy` but does not bubble up individual provider errors. +/// If no provider successfully responds, it will throw an error. +final public class FirstSuccessfulStrategy: Strategy { + public func evaluate( + providers: [FeatureProvider], + key: String, + defaultValue: T, + evaluationContext: EvaluationContext?, + flagEvaluation: FlagEvaluation + ) throws -> ProviderEvaluation where T: AllowedFlagValueType { + for provider in providers { + do { + let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) + if eval.errorCode == nil { + return eval + } + } catch { + continue + } + } + + throw OpenFeatureError.generalError( + message: "No provider returned a successful evaluation for the requested flag.") + } +} diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift new file mode 100644 index 0000000..635b932 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -0,0 +1,108 @@ +import Combine +import Foundation + +/// A provider that combines multiple providers into a single provider. +public class MultiProvider: FeatureProvider { + + public var hooks: [any Hook] { + [] + } + + public static let name = "MultiProvider" + public var metadata: ProviderMetadata = MultiProvider.MultiProviderMetadata() + + private let providers: [FeatureProvider] + private let strategy: Strategy + + /// Initialize a MultiProvider with a list of providers and a strategy. + /// - Parameters: + /// - providers: A list of providers to evaluate. + /// - strategy: A strategy to evaluate the providers. Defaults to FirstMatchStrategy. + public init( + providers: [FeatureProvider], + strategy: Strategy = FirstMatchStrategy() + ) { + self.providers = providers + self.strategy = strategy + } + + public func initialize(initialContext: EvaluationContext?) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for provider in providers { + group.addTask { + try await provider.initialize(initialContext: initialContext) + } + } + try await group.waitForAll() + } + } + + public func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for provider in providers { + group.addTask { + try await provider.onContextSet(oldContext: oldContext, newContext: newContext) + } + } + try await group.waitForAll() + } + } + + public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws + -> ProviderEvaluation + { + return try strategy.evaluate( + providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, + flagEvaluation: { provider in + return provider.getBooleanEvaluation(key:defaultValue:context:) + }) + } + + public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws + -> ProviderEvaluation + { + return try strategy.evaluate( + providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, + flagEvaluation: { provider in + return provider.getStringEvaluation(key:defaultValue:context:) + }) + } + + public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws + -> ProviderEvaluation + { + return try strategy.evaluate( + providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, + flagEvaluation: { provider in + return provider.getIntegerEvaluation(key:defaultValue:context:) + }) + } + + public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws + -> ProviderEvaluation + { + return try strategy.evaluate( + providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, + flagEvaluation: { provider in + return provider.getDoubleEvaluation(key:defaultValue:context:) + }) + } + + public func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws + -> ProviderEvaluation + { + return try strategy.evaluate( + providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, + flagEvaluation: { provider in + return provider.getObjectEvaluation(key:defaultValue:context:) + }) + } + + public func observe() -> AnyPublisher { + return Publishers.MergeMany(providers.map { $0.observe() }).eraseToAnyPublisher() + } + + public struct MultiProviderMetadata: ProviderMetadata { + public var name: String? = MultiProvider.name + } +} diff --git a/Sources/OpenFeature/Provider/MultiProvider/Strategy.swift b/Sources/OpenFeature/Provider/MultiProvider/Strategy.swift new file mode 100644 index 0000000..609e734 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/Strategy.swift @@ -0,0 +1,19 @@ +/// FlagEvaluation is a function that evaluates a feature flag and returns a ProviderEvaluation. +/// It is used to evaluate a feature flag across multiple providers using the strategy's logic. +public typealias FlagEvaluation = (FeatureProvider) -> ( + _ key: String, _ defaultValue: T, _ evaluationContext: EvaluationContext? +) throws -> ProviderEvaluation where T: AllowedFlagValueType + +/// Strategy interface defines how multiple feature providers should be evaluated +/// to determine the final result for a feature flag evaluation. +/// Different strategies can implement different logic for combining or selecting +/// results from multiple providers. +public protocol Strategy { + func evaluate( + providers: [FeatureProvider], + key: String, + defaultValue: T, + evaluationContext: EvaluationContext?, + flagEvaluation: FlagEvaluation + ) throws -> ProviderEvaluation where T: AllowedFlagValueType +} From df29538ed05d69c6f6e9d7dfb96d984ee1d6256a Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 25 Aug 2025 11:50:48 -0700 Subject: [PATCH 02/15] Resolve lint errors and run swift format Signed-off-by: jescriba --- .../MultiProvider/FirstMatchStrategy.swift | 1 - .../MultiProvider/MultiProvider.swift | 56 ++++++++++++------- Tests/OpenFeatureTests/EvalContextTests.swift | 20 ++++--- .../ImmutableContextTests.swift | 6 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift index 2e51ac4..17465f5 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -2,7 +2,6 @@ /// and returns the first result. Skips providers that indicate they had no value due to flag not found. /// If any provider returns an error result other than flag not found, the error is returned. final public class FirstMatchStrategy: Strategy { - public init() {} public func evaluate( diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift index 635b932..8751584 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -3,7 +3,6 @@ import Foundation /// A provider that combines multiple providers into a single provider. public class MultiProvider: FeatureProvider { - public var hooks: [any Hook] { [] } @@ -52,50 +51,65 @@ public class MultiProvider: FeatureProvider { -> ProviderEvaluation { return try strategy.evaluate( - providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, - flagEvaluation: { provider in - return provider.getBooleanEvaluation(key:defaultValue:context:) - }) + providers: providers, + key: key, + defaultValue: defaultValue, + evaluationContext: context + ) { provider in + return provider.getBooleanEvaluation(key:defaultValue:context:) + } } public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws -> ProviderEvaluation { return try strategy.evaluate( - providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, - flagEvaluation: { provider in - return provider.getStringEvaluation(key:defaultValue:context:) - }) + providers: providers, + key: key, + defaultValue: defaultValue, + evaluationContext: context + ) { provider in + return provider.getStringEvaluation(key:defaultValue:context:) + } } public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws -> ProviderEvaluation { return try strategy.evaluate( - providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, - flagEvaluation: { provider in - return provider.getIntegerEvaluation(key:defaultValue:context:) - }) + providers: providers, + key: key, + defaultValue: defaultValue, + evaluationContext: context + ) { provider in + return provider.getIntegerEvaluation(key:defaultValue:context:) + } } public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws -> ProviderEvaluation { return try strategy.evaluate( - providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, - flagEvaluation: { provider in - return provider.getDoubleEvaluation(key:defaultValue:context:) - }) + providers: providers, + key: key, + defaultValue: defaultValue, + evaluationContext: context + ) { provider in + return provider.getDoubleEvaluation(key:defaultValue:context:) + } } public func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws -> ProviderEvaluation { return try strategy.evaluate( - providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context, - flagEvaluation: { provider in - return provider.getObjectEvaluation(key:defaultValue:context:) - }) + providers: providers, + key: key, + defaultValue: defaultValue, + evaluationContext: context + ) { provider in + return provider.getObjectEvaluation(key:defaultValue:context:) + } } public func observe() -> AnyPublisher { diff --git a/Tests/OpenFeatureTests/EvalContextTests.swift b/Tests/OpenFeatureTests/EvalContextTests.swift index 42369dd..decf9ad 100644 --- a/Tests/OpenFeatureTests/EvalContextTests.swift +++ b/Tests/OpenFeatureTests/EvalContextTests.swift @@ -144,10 +144,12 @@ final class EvalContextTests: XCTestCase { originalContext.add(key: "integer", value: .integer(42)) originalContext.add(key: "boolean", value: .boolean(true)) originalContext.add(key: "list", value: .list([.string("item1"), .integer(100)])) - originalContext.add(key: "structure", value: .structure([ - "nested-string": .string("nested-value"), - "nested-int": .integer(200), - ])) + originalContext.add( + key: "structure", + value: .structure([ + "nested-string": .string("nested-value"), + "nested-int": .integer(200), + ])) guard let copiedContext = originalContext.deepCopy() as? MutableContext else { XCTFail("Failed to cast to MutableContext") @@ -207,10 +209,12 @@ final class EvalContextTests: XCTestCase { originalContext.add(key: "double", value: .double(3.14159)) originalContext.add(key: "date", value: .date(date)) originalContext.add(key: "list", value: .list([.string("list-item"), .integer(999)])) - originalContext.add(key: "structure", value: .structure([ - "struct-key": .string("struct-value"), - "struct-number": .integer(777), - ])) + originalContext.add( + key: "structure", + value: .structure([ + "struct-key": .string("struct-value"), + "struct-number": .integer(777), + ])) guard let copiedContext = originalContext.deepCopy() as? MutableContext else { XCTFail("Failed to cast to MutableContext") diff --git a/Tests/OpenFeatureTests/ImmutableContextTests.swift b/Tests/OpenFeatureTests/ImmutableContextTests.swift index 2557a31..187e494 100644 --- a/Tests/OpenFeatureTests/ImmutableContextTests.swift +++ b/Tests/OpenFeatureTests/ImmutableContextTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import OpenFeature final class ImmutableContextTests: XCTestCase { @@ -97,7 +98,7 @@ final class ImmutableContextTests: XCTestCase { // For null values, we need to check the unwrapped value let nullValue = objectMap["null"] - XCTAssertNil(nullValue as? AnyHashable) // But the unwrapped value is nil + XCTAssertNil(nullValue as? AnyHashable) // But the unwrapped value is nil } func testImmutableContextWithTargetingKey() { @@ -182,7 +183,8 @@ final class ImmutableContextTests: XCTestCase { expectation.expectedFulfillmentCount = 10 DispatchQueue.concurrentPerform(iterations: 10) { index in - let modified = original + let modified = + original .withAttribute(key: "thread", value: .integer(Int64(index))) .withAttribute(key: "timestamp", value: .double(Double(index))) From 06b24a9f5d8b3f3b11f9396ee5195c814c90c76a Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 25 Aug 2025 15:23:59 -0700 Subject: [PATCH 03/15] Add unit tests for multiprovider Signed-off-by: jescriba --- .../FirstSuccessfulStrategy.swift | 3 +- .../Helpers/MockProvider.swift | 135 +++++++++ .../OpenFeatureTests/MultiProviderTests.swift | 257 ++++++++++++++++++ 3 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 Tests/OpenFeatureTests/Helpers/MockProvider.swift create mode 100644 Tests/OpenFeatureTests/MultiProviderTests.swift diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift index 003a8a8..2edb35d 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -20,7 +20,6 @@ final public class FirstSuccessfulStrategy: Strategy { } } - throw OpenFeatureError.generalError( - message: "No provider returned a successful evaluation for the requested flag.") + throw OpenFeatureError.flagNotFoundError(key: key) } } diff --git a/Tests/OpenFeatureTests/Helpers/MockProvider.swift b/Tests/OpenFeatureTests/Helpers/MockProvider.swift new file mode 100644 index 0000000..6d6c976 --- /dev/null +++ b/Tests/OpenFeatureTests/Helpers/MockProvider.swift @@ -0,0 +1,135 @@ +import Combine +import Foundation + +@testable import OpenFeature + +/// A mock provider that can be used to test provider events with payloads. +/// It can be configured with a set of callbacks that will be called when the provider is initialized +class MockProvider: FeatureProvider { + static let name = "MockProvider" + var metadata: ProviderMetadata = MockProviderMetadata() + + var hooks: [any Hook] = [] + var throwFatal = false + private let eventHandler = EventHandler() + private let _onContextSet: (EvaluationContext?, EvaluationContext) async throws -> Void + private let _initialize: (EvaluationContext?) async throws -> Void + private let _getBooleanEvaluation: (String, Bool, EvaluationContext?) throws -> ProviderEvaluation + private let _getStringEvaluation: (String, String, EvaluationContext?) throws -> ProviderEvaluation + private let _getIntegerEvaluation: (String, Int64, EvaluationContext?) throws -> ProviderEvaluation + private let _getDoubleEvaluation: (String, Double, EvaluationContext?) throws -> ProviderEvaluation + private let _getObjectEvaluation: (String, Value, EvaluationContext?) throws -> ProviderEvaluation + private let _observe: () -> AnyPublisher + + /// Initialize the provider with a set of callbacks that will be called when the provider is initialized, + init( + onContextSet: @escaping (EvaluationContext?, EvaluationContext) async throws -> Void = { _, _ in }, + initialize: @escaping (EvaluationContext?) async throws -> Void = { _ in }, + getBooleanEvaluation: @escaping ( + String, + Bool, + EvaluationContext? + ) throws -> ProviderEvaluation = { _, fallback, _ in + return ProviderEvaluation(value: fallback, flagMetadata: [:]) + }, + getStringEvaluation: @escaping ( + String, + String, + EvaluationContext? + ) throws -> ProviderEvaluation = { _, fallback, _ in + return ProviderEvaluation(value: fallback, flagMetadata: [:]) + }, + getIntegerEvaluation: @escaping ( + String, + Int64, + EvaluationContext? + ) throws -> ProviderEvaluation = { _, fallback, _ in + return ProviderEvaluation(value: fallback, flagMetadata: [:]) + }, + getDoubleEvaluation: @escaping ( + String, + Double, + EvaluationContext? + ) throws -> ProviderEvaluation = { _, fallback, _ in + return ProviderEvaluation(value: fallback, flagMetadata: [:]) + }, + getObjectEvaluation: @escaping ( + String, + Value, + EvaluationContext? + ) throws -> ProviderEvaluation = { _, fallback, _ in + return ProviderEvaluation(value: fallback, flagMetadata: [:]) + }, + observe: @escaping () -> AnyPublisher = { Just(nil).eraseToAnyPublisher() } + ) { + self._onContextSet = onContextSet + self._initialize = initialize + self._getBooleanEvaluation = getBooleanEvaluation + self._getStringEvaluation = getStringEvaluation + self._getIntegerEvaluation = getIntegerEvaluation + self._getDoubleEvaluation = getDoubleEvaluation + self._getObjectEvaluation = getObjectEvaluation + self._observe = observe + } + + func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) async throws { + try await _onContextSet(oldContext, newContext) + } + + func initialize(initialContext: EvaluationContext?) async throws { + try await _initialize(initialContext) + } + + func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws + -> ProviderEvaluation + { + try _getBooleanEvaluation(key, defaultValue, context) + } + + func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws + -> ProviderEvaluation + { + try _getStringEvaluation(key, defaultValue, context) + } + + func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws + -> ProviderEvaluation + { + try _getIntegerEvaluation(key, defaultValue, context) + } + + func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws + -> ProviderEvaluation + { + try _getDoubleEvaluation(key, defaultValue, context) + } + + func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws + -> ProviderEvaluation + { + try _getObjectEvaluation(key, defaultValue, context) + } + + func observe() -> AnyPublisher { + _observe() + } +} + +extension MockProvider { + struct MockProviderMetadata: ProviderMetadata { + var name: String? = MockProvider.name + } +} + +extension MockProvider { + enum MockProviderError: LocalizedError { + case message(String) + + var errorDescription: String? { + switch self { + case .message(let message): + return message + } + } + } +} \ No newline at end of file diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift new file mode 100644 index 0000000..1d6088c --- /dev/null +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -0,0 +1,257 @@ +import Combine +import XCTest + +@testable import OpenFeature + +final class MultiProviderTests: XCTestCase { + func testEvaluationWithMultipleProvidersDefaultStrategy_MultipleTypes() throws { + // Test first provider missing flag results in second provider being evaluated + let mockKey = "testKey" + let mockProviderBoolValue = true + let mockProviderStringValue = "testString" + let mockProviderIntegerValue: Int64 = 1 + let mockProviderDoubleValue: Double = 1.0 + let mockProviderObjectValue: Value = Value.structure(["testKey": Value.string("testValue")]) + // First provider doesn't have the flag and test using all types + let mockProvider1 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + throw OpenFeatureError.flagNotFoundError(key: flag) + }, + getStringEvaluation: { flag, defaultValue, _ in + throw OpenFeatureError.flagNotFoundError(key: flag) + }, + getIntegerEvaluation: { flag, defaultValue, _ in + throw OpenFeatureError.flagNotFoundError(key: flag) + }, + getDoubleEvaluation: { flag, defaultValue, _ in + throw OpenFeatureError.flagNotFoundError(key: flag) + }, + getObjectEvaluation: { flag, defaultValue, _ in + throw OpenFeatureError.flagNotFoundError(key: flag) + } + ) + // Second provider has the flag and test using all types + let mockProvider2 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderBoolValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + }, + getStringEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderStringValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + }, + getIntegerEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderIntegerValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + }, + getDoubleEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderDoubleValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + }, + getObjectEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderObjectValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + } + ) + let multiProvider = MultiProvider(providers: [mockProvider1, mockProvider2]) + + // Expect the second provider's value to be returned + let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTAssertEqual(boolResult.value, mockProviderBoolValue) + let stringResult = try multiProvider.getStringEvaluation(key: mockKey, defaultValue: "", context: MutableContext()) + XCTAssertEqual(stringResult.value, mockProviderStringValue) + let integerResult = try multiProvider.getIntegerEvaluation(key: mockKey, defaultValue: 0, context: MutableContext()) + XCTAssertEqual(integerResult.value, mockProviderIntegerValue) + let doubleResult = try multiProvider.getDoubleEvaluation(key: mockKey, defaultValue: 0.0, context: MutableContext()) + XCTAssertEqual(doubleResult.value, mockProviderDoubleValue) + let objectResult = try multiProvider.getObjectEvaluation(key: mockKey, defaultValue: .null, context: MutableContext()) + XCTAssertEqual(objectResult.value, mockProviderObjectValue) + } + + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FirstProviderHasFlag() throws { + let mockKey = "test-key" + let mockProvider1Value = true + let mockProvider1 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: mockProvider1Value) + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { key, _, _ in + return ProviderEvaluation(value: !mockProvider1Value) + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstMatchStrategy() + ) + + let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTAssertEqual(boolResult.value, mockProvider1Value) + } + + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FlagNotFound() throws { + let mockKey = "test-key" + let mockProviderValue = true + let mockProvider1 = MockProvider( + getBooleanEvaluation: { key, _, _ in + throw OpenFeatureError.flagNotFoundError(key: key) + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + if flag == mockKey { + return ProviderEvaluation(value: mockProviderValue) + } else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstMatchStrategy() + ) + + let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTAssertEqual(boolResult.value, mockProviderValue) + } + + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_AllProvidersMissingFlag() throws { + let mockKey = "test-key" + let mockProvider1 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { key, _, _ in + throw OpenFeatureError.flagNotFoundError(key: key) + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstMatchStrategy() + ) + + let result = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTAssertTrue(result.errorCode == .flagNotFound) + } + + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { + let mockKey = "test-key" + let mockProvider1 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { _, _, _ in + throw OpenFeatureError.generalError(message: "test error") + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstMatchStrategy() + ) + + do { + _ = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTFail("Expected error to be thrown") + } catch { + XCTAssertEqual(error as? OpenFeatureError, OpenFeatureError.generalError(message: "test error")) + } + } + + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_HandlesError() throws { + let mockKey = "test-key" + let mockProvider1Value = true + let mockProvider1 = MockProvider( + getBooleanEvaluation: { _, _, _ in + throw OpenFeatureError.generalError(message: "test error") + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: mockProvider1Value) + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstSuccessfulStrategy() + ) + + let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTAssertEqual(boolResult.value, mockProvider1Value) + XCTAssertNil(boolResult.errorCode) + } + + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_MissingFlag() throws { + let mockKey = "test-key" + let mockProvider1 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstSuccessfulStrategy() + ) + + do { + let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + XCTFail("Expected error to be thrown") + } catch { + XCTAssertTrue(error as? OpenFeatureError == OpenFeatureError.flagNotFoundError(key: mockKey)) + } + } + + func testObserveWithMultipleProviders() { + let mockEvent1 = ProviderEvent.ready + let mockProvider1 = MockProvider( + observe: { + return Just(mockEvent1).eraseToAnyPublisher() + } + ) + let mockEvent2 = ProviderEvent.contextChanged + let mockProvider2 = MockProvider( + observe: { + return Just(mockEvent2).eraseToAnyPublisher() + } + ) + let multiProvider = MultiProvider(providers: [mockProvider1, mockProvider2]) + let fulfillment = XCTestExpectation(description: "Received provider events") + let mockEvents = [mockEvent1, mockEvent2] + var receivedEvents: [ProviderEvent] = [] + let observation = multiProvider.observe().sink(receiveValue: { event in + if let event { + receivedEvents.append(event) + } + if receivedEvents.count == mockEvents.count { + fulfillment.fulfill() + } + }) + wait(for: [fulfillment], timeout: 2) + observation.cancel() + XCTAssertEqual(receivedEvents, mockEvents) + } +} From 4863f2fd81a6073deead8afdf02fb8e143e61d21 Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 25 Aug 2025 16:13:32 -0700 Subject: [PATCH 04/15] Resolve lint and format Signed-off-by: jescriba --- .../Helpers/MockProvider.swift | 6 +- .../OpenFeatureTests/MultiProviderTests.swift | 242 ++++++++++-------- 2 files changed, 140 insertions(+), 108 deletions(-) diff --git a/Tests/OpenFeatureTests/Helpers/MockProvider.swift b/Tests/OpenFeatureTests/Helpers/MockProvider.swift index 6d6c976..eed522f 100644 --- a/Tests/OpenFeatureTests/Helpers/MockProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/MockProvider.swift @@ -43,7 +43,7 @@ class MockProvider: FeatureProvider { String, Int64, EvaluationContext? - ) throws -> ProviderEvaluation = { _, fallback, _ in + ) throws -> ProviderEvaluation = { _, fallback, _ in return ProviderEvaluation(value: fallback, flagMetadata: [:]) }, getDoubleEvaluation: @escaping ( @@ -57,7 +57,7 @@ class MockProvider: FeatureProvider { String, Value, EvaluationContext? - ) throws -> ProviderEvaluation = { _, fallback, _ in + ) throws -> ProviderEvaluation = { _, fallback, _ in return ProviderEvaluation(value: fallback, flagMetadata: [:]) }, observe: @escaping () -> AnyPublisher = { Just(nil).eraseToAnyPublisher() } @@ -132,4 +132,4 @@ extension MockProvider { } } } -} \ No newline at end of file +} diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 1d6088c..022f4f3 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -11,109 +11,72 @@ final class MultiProviderTests: XCTestCase { let mockProviderStringValue = "testString" let mockProviderIntegerValue: Int64 = 1 let mockProviderDoubleValue: Double = 1.0 - let mockProviderObjectValue: Value = Value.structure(["testKey": Value.string("testValue")]) + let mockProviderObjectValue = Value.structure(["testKey": Value.string("testValue")]) + let mockError = OpenFeatureError.flagNotFoundError(key: mockKey) // First provider doesn't have the flag and test using all types - let mockProvider1 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in - throw OpenFeatureError.flagNotFoundError(key: flag) - }, - getStringEvaluation: { flag, defaultValue, _ in - throw OpenFeatureError.flagNotFoundError(key: flag) - }, - getIntegerEvaluation: { flag, defaultValue, _ in - throw OpenFeatureError.flagNotFoundError(key: flag) - }, - getDoubleEvaluation: { flag, defaultValue, _ in - throw OpenFeatureError.flagNotFoundError(key: flag) - }, - getObjectEvaluation: { flag, defaultValue, _ in - throw OpenFeatureError.flagNotFoundError(key: flag) - } - ) + let mockProvider1 = MultiProviderTestHelpers.mockThrowingProvider(error: mockError) // Second provider has the flag and test using all types - let mockProvider2 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in - if flag == mockKey { - return ProviderEvaluation(value: mockProviderBoolValue) - } else { - return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) - } - }, - getStringEvaluation: { flag, defaultValue, _ in - if flag == mockKey { - return ProviderEvaluation(value: mockProviderStringValue) - } else { - return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) - } - }, - getIntegerEvaluation: { flag, defaultValue, _ in - if flag == mockKey { - return ProviderEvaluation(value: mockProviderIntegerValue) - } else { - return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) - } - }, - getDoubleEvaluation: { flag, defaultValue, _ in - if flag == mockKey { - return ProviderEvaluation(value: mockProviderDoubleValue) - } else { - return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) - } - }, - getObjectEvaluation: { flag, defaultValue, _ in - if flag == mockKey { - return ProviderEvaluation(value: mockProviderObjectValue) - } else { - return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) - } - } + let mockProvider2 = MultiProviderTestHelpers.mockTestProvider( + values: MultiProviderTestHelpers.MockValues( + mockKey: mockKey, + mockProviderBoolValue: mockProviderBoolValue, + mockProviderStringValue: mockProviderStringValue, + mockProviderIntegerValue: mockProviderIntegerValue, + mockProviderDoubleValue: mockProviderDoubleValue, + mockProviderObjectValue: mockProviderObjectValue + ) ) let multiProvider = MultiProvider(providers: [mockProvider1, mockProvider2]) - // Expect the second provider's value to be returned - let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + let boolResult = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: false, context: MutableContext()) XCTAssertEqual(boolResult.value, mockProviderBoolValue) - let stringResult = try multiProvider.getStringEvaluation(key: mockKey, defaultValue: "", context: MutableContext()) + let stringResult = try multiProvider.getStringEvaluation( + key: mockKey, defaultValue: "", context: MutableContext()) XCTAssertEqual(stringResult.value, mockProviderStringValue) - let integerResult = try multiProvider.getIntegerEvaluation(key: mockKey, defaultValue: 0, context: MutableContext()) + let integerResult = try multiProvider.getIntegerEvaluation( + key: mockKey, defaultValue: 0, context: MutableContext()) XCTAssertEqual(integerResult.value, mockProviderIntegerValue) - let doubleResult = try multiProvider.getDoubleEvaluation(key: mockKey, defaultValue: 0.0, context: MutableContext()) + let doubleResult = try multiProvider.getDoubleEvaluation( + key: mockKey, defaultValue: 0.0, context: MutableContext()) XCTAssertEqual(doubleResult.value, mockProviderDoubleValue) - let objectResult = try multiProvider.getObjectEvaluation(key: mockKey, defaultValue: .null, context: MutableContext()) + let objectResult = try multiProvider.getObjectEvaluation( + key: mockKey, defaultValue: .null, context: MutableContext()) XCTAssertEqual(objectResult.value, mockProviderObjectValue) } - + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FirstProviderHasFlag() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in - return ProviderEvaluation(value: mockProvider1Value) - } + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in ProviderEvaluation(value: mockProvider1Value) } ) let mockProvider2 = MockProvider( - getBooleanEvaluation: { key, _, _ in - return ProviderEvaluation(value: !mockProvider1Value) - } + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in ProviderEvaluation(value: !mockProvider1Value) } ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], strategy: FirstMatchStrategy() ) - - let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + + let boolResult = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: false, context: MutableContext()) XCTAssertEqual(boolResult.value, mockProvider1Value) } - + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FlagNotFound() throws { let mockKey = "test-key" let mockProviderValue = true let mockProvider1 = MockProvider( + initialize: { _ in }, getBooleanEvaluation: { key, _, _ in throw OpenFeatureError.flagNotFoundError(key: key) } ) let mockProvider2 = MockProvider( + initialize: { _ in }, getBooleanEvaluation: { flag, defaultValue, _ in if flag == mockKey { return ProviderEvaluation(value: mockProviderValue) @@ -126,40 +89,47 @@ final class MultiProviderTests: XCTestCase { providers: [mockProvider1, mockProvider2], strategy: FirstMatchStrategy() ) - - let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + + let boolResult = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: false, context: MutableContext()) XCTAssertEqual(boolResult.value, mockProviderValue) } - + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_AllProvidersMissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) } ) let mockProvider2 = MockProvider( - getBooleanEvaluation: { key, _, _ in - throw OpenFeatureError.flagNotFoundError(key: key) - } + initialize: { _ in }, + getBooleanEvaluation: { key, _, _ in throw OpenFeatureError.flagNotFoundError(key: key) } ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], strategy: FirstMatchStrategy() ) - - let result = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + + let result = try multiProvider.getBooleanEvaluation( + key: mockKey, + defaultValue: false, + context: MutableContext() + ) XCTAssertTrue(result.errorCode == .flagNotFound) } - + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) } ) let mockProvider2 = MockProvider( + initialize: { _ in }, getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") } @@ -168,7 +138,7 @@ final class MultiProviderTests: XCTestCase { providers: [mockProvider1, mockProvider2], strategy: FirstMatchStrategy() ) - + do { _ = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) XCTFail("Expected error to be thrown") @@ -176,17 +146,19 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(error as? OpenFeatureError, OpenFeatureError.generalError(message: "test error")) } } - + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_HandlesError() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( + initialize: { _ in }, getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") } ) let mockProvider2 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in return ProviderEvaluation(value: mockProvider1Value) } ) @@ -194,21 +166,24 @@ final class MultiProviderTests: XCTestCase { providers: [mockProvider1, mockProvider2], strategy: FirstSuccessfulStrategy() ) - - let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + + let boolResult = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: false, context: MutableContext()) XCTAssertEqual(boolResult.value, mockProvider1Value) XCTAssertNil(boolResult.errorCode) } - + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_MissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) } ) let mockProvider2 = MockProvider( - getBooleanEvaluation: { flag, defaultValue, _ in + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) } ) @@ -216,42 +191,99 @@ final class MultiProviderTests: XCTestCase { providers: [mockProvider1, mockProvider2], strategy: FirstSuccessfulStrategy() ) - + do { - let boolResult = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) + _ = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) XCTFail("Expected error to be thrown") } catch { XCTAssertTrue(error as? OpenFeatureError == OpenFeatureError.flagNotFoundError(key: mockKey)) } } - + func testObserveWithMultipleProviders() { let mockEvent1 = ProviderEvent.ready let mockProvider1 = MockProvider( - observe: { - return Just(mockEvent1).eraseToAnyPublisher() - } + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { Just(mockEvent1).eraseToAnyPublisher() } ) let mockEvent2 = ProviderEvent.contextChanged let mockProvider2 = MockProvider( - observe: { - return Just(mockEvent2).eraseToAnyPublisher() - } + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { Just(mockEvent2).eraseToAnyPublisher() } ) let multiProvider = MultiProvider(providers: [mockProvider1, mockProvider2]) let fulfillment = XCTestExpectation(description: "Received provider events") let mockEvents = [mockEvent1, mockEvent2] var receivedEvents: [ProviderEvent] = [] - let observation = multiProvider.observe().sink(receiveValue: { event in - if let event { - receivedEvents.append(event) - } - if receivedEvents.count == mockEvents.count { - fulfillment.fulfill() + let observation = + multiProvider + .observe() + .sink { event in + if let event { + receivedEvents.append(event) + } + if receivedEvents.count == mockEvents.count { + fulfillment.fulfill() + } } - }) wait(for: [fulfillment], timeout: 2) observation.cancel() XCTAssertEqual(receivedEvents, mockEvents) } } + +enum MultiProviderTestHelpers { + static func mockThrowingProvider(error: OpenFeatureError) -> MockProvider { + return MockProvider( + getBooleanEvaluation: { _, _, _ in throw error }, + getStringEvaluation: { _, _, _ in throw error }, + getIntegerEvaluation: { _, _, _ in throw error }, + getDoubleEvaluation: { _, _, _ in throw error }, + getObjectEvaluation: { _, _, _ in throw error } + ) + } + + struct MockValues { + let mockKey: String + let mockProviderBoolValue: Bool + let mockProviderStringValue: String + let mockProviderIntegerValue: Int64 + let mockProviderDoubleValue: Double + let mockProviderObjectValue: Value + } + + static func mockTestProvider(values: MockValues) -> MockProvider { + MockProvider( + getBooleanEvaluation: { flag, defaultValue, _ in + guard flag == values.mockKey else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + return ProviderEvaluation(value: values.mockProviderBoolValue) + }, + getStringEvaluation: { flag, defaultValue, _ in + guard flag == values.mockKey else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + return ProviderEvaluation(value: values.mockProviderStringValue) + }, + getIntegerEvaluation: { flag, defaultValue, _ in + guard flag == values.mockKey else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + return ProviderEvaluation(value: values.mockProviderIntegerValue) + }, + getDoubleEvaluation: { flag, defaultValue, _ in + guard flag == values.mockKey else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + return ProviderEvaluation(value: values.mockProviderDoubleValue) + }, + getObjectEvaluation: { flag, defaultValue, _ in + guard flag == values.mockKey else { + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + return ProviderEvaluation(value: values.mockProviderObjectValue) + } + ) + } +} From 254cb55f56810d0d42e5801ea757ccb3fb3b03a5 Mon Sep 17 00:00:00 2001 From: jescriba Date: Tue, 26 Aug 2025 11:16:48 -0700 Subject: [PATCH 05/15] Updates returning an error vs throwing an error pattern in multiprovider strategies Signed-off-by: jescriba --- .../MultiProvider/FirstMatchStrategy.swift | 7 +++ .../FirstSuccessfulStrategy.swift | 8 +++- .../OpenFeatureTests/MultiProviderTests.swift | 46 +++++++++++++++---- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift index 17465f5..f860c7e 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -19,6 +19,13 @@ final public class FirstMatchStrategy: Strategy { } } catch OpenFeatureError.flagNotFoundError { continue + } catch let error as OpenFeatureError { + return ProviderEvaluation( + value: defaultValue, + reason: Reason.error.rawValue, + errorCode: error.errorCode(), + errorMessage: error.description + ) } catch { throw error } diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift index 2edb35d..2784d02 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -1,6 +1,6 @@ /// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers /// and returns the first result. Similar to `FirstMatchStrategy` but does not bubble up individual provider errors. -/// If no provider successfully responds, it will throw an error. +/// If no provider successfully responds, it will return an error. final public class FirstSuccessfulStrategy: Strategy { public func evaluate( providers: [FeatureProvider], @@ -20,6 +20,10 @@ final public class FirstSuccessfulStrategy: Strategy { } } - throw OpenFeatureError.flagNotFoundError(key: key) + return ProviderEvaluation( + value: defaultValue, + reason: Reason.defaultReason.rawValue, + errorCode: ErrorCode.flagNotFound + ) } } diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 022f4f3..7fe56bf 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -120,7 +120,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertTrue(result.errorCode == .flagNotFound) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesOpenFeatureError() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -138,12 +138,39 @@ final class MultiProviderTests: XCTestCase { providers: [mockProvider1, mockProvider2], strategy: FirstMatchStrategy() ) + let defaultValue = false + let result = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: defaultValue, context: MutableContext()) + XCTAssertEqual(result.value, false) + XCTAssertNotNil(result.errorCode) + } + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { + let mockKey = "test-key" + let mockError = MockProvider.MockProviderError.message("test non-open feature error") + let mockProvider1 = MockProvider( + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in + throw mockError + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstMatchStrategy() + ) + let defaultValue = false do { - _ = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) - XCTFail("Expected error to be thrown") + _ = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: defaultValue, context: MutableContext()) + XCTFail("Expected to throw") } catch { - XCTAssertEqual(error as? OpenFeatureError, OpenFeatureError.generalError(message: "test error")) + XCTAssertTrue(error is MockProvider.MockProviderError) } } @@ -192,12 +219,11 @@ final class MultiProviderTests: XCTestCase { strategy: FirstSuccessfulStrategy() ) - do { - _ = try multiProvider.getBooleanEvaluation(key: mockKey, defaultValue: false, context: MutableContext()) - XCTFail("Expected error to be thrown") - } catch { - XCTAssertTrue(error as? OpenFeatureError == OpenFeatureError.flagNotFoundError(key: mockKey)) - } + let defaultValue = false + let result = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: defaultValue, context: MutableContext()) + XCTAssertEqual(result.errorCode, .flagNotFound) + XCTAssertEqual(result.value, defaultValue) } func testObserveWithMultipleProviders() { From 57e54a24c09384d07eeee6da10a4294b25799011 Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 8 Sep 2025 11:11:55 -0700 Subject: [PATCH 06/15] Try iPhone 16 to resolve missing sim issues in CI Signed-off-by: jescriba --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dfeb126..3eb63bf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: platform: [iOS, macOS, watchOS, tvOS] include: - platform: iOS - destination: "platform=iOS Simulator,name=iPhone 15" + destination: "platform=iOS Simulator,name=iPhone 16" - platform: macOS destination: "platform=macOS" - platform: watchOS From a74e30f58f1bd1ea326586729d3f2641b74aed2a Mon Sep 17 00:00:00 2001 From: jescriba Date: Mon, 8 Sep 2025 12:11:40 -0700 Subject: [PATCH 07/15] Remove unused event handler and address gemini review comment Signed-off-by: jescriba --- Tests/OpenFeatureTests/Helpers/MockProvider.swift | 1 - Tests/OpenFeatureTests/MultiProviderTests.swift | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/OpenFeatureTests/Helpers/MockProvider.swift b/Tests/OpenFeatureTests/Helpers/MockProvider.swift index eed522f..ae4bc3c 100644 --- a/Tests/OpenFeatureTests/Helpers/MockProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/MockProvider.swift @@ -11,7 +11,6 @@ class MockProvider: FeatureProvider { var hooks: [any Hook] = [] var throwFatal = false - private let eventHandler = EventHandler() private let _onContextSet: (EvaluationContext?, EvaluationContext) async throws -> Void private let _initialize: (EvaluationContext?) async throws -> Void private let _getBooleanEvaluation: (String, Bool, EvaluationContext?) throws -> ProviderEvaluation diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 7fe56bf..41b2595 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -254,7 +254,9 @@ final class MultiProviderTests: XCTestCase { } wait(for: [fulfillment], timeout: 2) observation.cancel() - XCTAssertEqual(receivedEvents, mockEvents) + XCTAssertEqual(receivedEvents.count, mockEvents.count) + XCTAssertTrue(receivedEvents.contains(mockEvent1)) + XCTAssertTrue(receivedEvents.contains(mockEvent2)) } } From f527a7b8308fdaa9283f670152633e51cbb7af43 Mon Sep 17 00:00:00 2001 From: jescriba Date: Wed, 10 Sep 2025 17:14:13 -0700 Subject: [PATCH 08/15] Update multiprovider name and FirstMatchStrategy to FirstFoundStrategy Signed-off-by: jescriba --- ...trategy.swift => FirstFoundStrategy.swift} | 4 ++-- .../FirstSuccessfulStrategy.swift | 2 +- .../MultiProvider/MultiProvider.swift | 15 ++++++++++---- .../OpenFeatureTests/MultiProviderTests.swift | 20 +++++++++---------- 4 files changed, 24 insertions(+), 17 deletions(-) rename Sources/OpenFeature/Provider/MultiProvider/{FirstMatchStrategy.swift => FirstFoundStrategy.swift} (89%) diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift similarity index 89% rename from Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift rename to Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift index f860c7e..4f0578e 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift @@ -1,7 +1,7 @@ -/// FirstMatchStrategy is a strategy that evaluates a feature flag across multiple providers +/// FirstFoundStrategy (FirstMatchStrategy) is a strategy that evaluates a feature flag across multiple providers /// and returns the first result. Skips providers that indicate they had no value due to flag not found. /// If any provider returns an error result other than flag not found, the error is returned. -final public class FirstMatchStrategy: Strategy { +final public class FirstFoundStrategy: Strategy { public init() {} public func evaluate( diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift index 2784d02..3897926 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -1,5 +1,5 @@ /// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers -/// and returns the first result. Similar to `FirstMatchStrategy` but does not bubble up individual provider errors. +/// and returns the first result. Similar to `FirstFoundStrategy` but does not bubble up individual provider errors. /// If no provider successfully responds, it will return an error. final public class FirstSuccessfulStrategy: Strategy { public func evaluate( diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift index 8751584..920d114 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -8,7 +8,7 @@ public class MultiProvider: FeatureProvider { } public static let name = "MultiProvider" - public var metadata: ProviderMetadata = MultiProvider.MultiProviderMetadata() + public var metadata: ProviderMetadata private let providers: [FeatureProvider] private let strategy: Strategy @@ -16,13 +16,14 @@ public class MultiProvider: FeatureProvider { /// Initialize a MultiProvider with a list of providers and a strategy. /// - Parameters: /// - providers: A list of providers to evaluate. - /// - strategy: A strategy to evaluate the providers. Defaults to FirstMatchStrategy. + /// - strategy: A strategy to evaluate the providers. Defaults to FirstFoundStrategy. public init( providers: [FeatureProvider], - strategy: Strategy = FirstMatchStrategy() + strategy: Strategy = FirstFoundStrategy() ) { self.providers = providers self.strategy = strategy + self.metadata = MultiProviderMetadata(providers: providers) } public func initialize(initialContext: EvaluationContext?) async throws { @@ -117,6 +118,12 @@ public class MultiProvider: FeatureProvider { } public struct MultiProviderMetadata: ProviderMetadata { - public var name: String? = MultiProvider.name + public var name: String? + + init(providers: [FeatureProvider]) { + self.name = providers.map({ + $0.metadata.name ?? "MultiProvider" + }).joined(separator: ", ") + } } } diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 41b2595..4e01a6e 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -45,7 +45,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(objectResult.value, mockProviderObjectValue) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FirstProviderHasFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_FirstProviderHasFlag() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( @@ -58,7 +58,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstFoundStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -66,7 +66,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(boolResult.value, mockProvider1Value) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FlagNotFound() throws { + func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_FlagNotFound() throws { let mockKey = "test-key" let mockProviderValue = true let mockProvider1 = MockProvider( @@ -87,7 +87,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstFoundStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -95,7 +95,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(boolResult.value, mockProviderValue) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_AllProvidersMissingFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_AllProvidersMissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -109,7 +109,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstFoundStrategy() ) let result = try multiProvider.getBooleanEvaluation( @@ -120,7 +120,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertTrue(result.errorCode == .flagNotFound) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesOpenFeatureError() throws { + func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_HandlesOpenFeatureError() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -136,7 +136,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstFoundStrategy() ) let defaultValue = false let result = try multiProvider.getBooleanEvaluation( @@ -145,7 +145,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertNotNil(result.errorCode) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { + func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_Throws() throws { let mockKey = "test-key" let mockError = MockProvider.MockProviderError.message("test non-open feature error") let mockProvider1 = MockProvider( @@ -162,7 +162,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstFoundStrategy() ) let defaultValue = false do { From 65f9af28e7753a11ee509b7c91dd7db85bdfc44d Mon Sep 17 00:00:00 2001 From: jescriba Date: Wed, 10 Sep 2025 17:43:10 -0700 Subject: [PATCH 09/15] Add developer experience tests for multiprovider Signed-off-by: jescriba --- .../DeveloperExperienceTests.swift | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index 1bd1b28..39feec9 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -1,3 +1,4 @@ +import Combine import XCTest @testable import OpenFeature @@ -185,4 +186,68 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(details.errorMessage, "A fatal error occurred in the provider: unknown") XCTAssertEqual(details.reason, Reason.error.rawValue) } + + func testMultiProviderObserveEvents() async { + let mockEvent1Subject = CurrentValueSubject(nil) + let mockEvent2Subject = CurrentValueSubject(nil) + // Create test providers that can emit events + let eventEmittingProvider1 = MockProvider( + initialize: { _ in mockEvent1Subject.send(.ready) }, + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { mockEvent1Subject.eraseToAnyPublisher() } + ) + let eventEmittingProvider2 = MockProvider( + initialize: { _ in mockEvent2Subject.send(.ready) }, + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { mockEvent2Subject.eraseToAnyPublisher() } + ) + + // Create MultiProvider with both providers + let multiProvider = MultiProvider(providers: [eventEmittingProvider1, eventEmittingProvider2]) + + // Set up expectations for different events + let readyExpectation = XCTestExpectation(description: "Ready event received") + let configChangedExpectation = XCTestExpectation(description: "Configuration changed event received") + let errorExpectation = XCTestExpectation(description: "Error event received") + + var receivedEvents: [ProviderEvent] = [] + + // Observe events from MultiProvider + let observer = multiProvider.observe().sink { event in + guard let event = event else { return } + receivedEvents.append(event) + + switch event { + case .ready: + readyExpectation.fulfill() + case .configurationChanged: + configChangedExpectation.fulfill() + case .error: + errorExpectation.fulfill() + default: + break + } + } + + // Set the MultiProvider in OpenFeatureAPI to test integration + await OpenFeatureAPI.shared.setProviderAndWait(provider: multiProvider) + + // Emit events from the first provider + mockEvent1Subject.send(.ready) + mockEvent1Subject.send(.configurationChanged) + + // Emit events from the second provider + mockEvent2Subject.send(.error(errorCode: .general, message: "Test error")) + + // Wait for all events to be received + await fulfillment(of: [readyExpectation, configChangedExpectation, errorExpectation], timeout: 2) + + // Verify that events from both providers were received + XCTAssertTrue(receivedEvents.contains(.ready)) + XCTAssertTrue(receivedEvents.contains(.configurationChanged)) + XCTAssertTrue(receivedEvents.contains(.error(errorCode: .general, message: "Test error"))) + XCTAssertGreaterThanOrEqual(receivedEvents.count, 3) + + observer.cancel() + } } From 62c01ada8762738b4cddd904ae749c16cf9464fd Mon Sep 17 00:00:00 2001 From: jescriba Date: Wed, 10 Sep 2025 17:48:54 -0700 Subject: [PATCH 10/15] Run lint fixes Signed-off-by: jescriba --- .../MultiProvider/MultiProvider.swift | 19 ++++++++++--------- .../DeveloperExperienceTests.swift | 18 +++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift index 920d114..b3ae466 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -23,7 +23,7 @@ public class MultiProvider: FeatureProvider { ) { self.providers = providers self.strategy = strategy - self.metadata = MultiProviderMetadata(providers: providers) + metadata = MultiProviderMetadata(providers: providers) } public func initialize(initialContext: EvaluationContext?) async throws { @@ -57,7 +57,7 @@ public class MultiProvider: FeatureProvider { defaultValue: defaultValue, evaluationContext: context ) { provider in - return provider.getBooleanEvaluation(key:defaultValue:context:) + provider.getBooleanEvaluation(key:defaultValue:context:) } } @@ -70,7 +70,7 @@ public class MultiProvider: FeatureProvider { defaultValue: defaultValue, evaluationContext: context ) { provider in - return provider.getStringEvaluation(key:defaultValue:context:) + provider.getStringEvaluation(key:defaultValue:context:) } } @@ -83,7 +83,7 @@ public class MultiProvider: FeatureProvider { defaultValue: defaultValue, evaluationContext: context ) { provider in - return provider.getIntegerEvaluation(key:defaultValue:context:) + provider.getIntegerEvaluation(key:defaultValue:context:) } } @@ -96,7 +96,7 @@ public class MultiProvider: FeatureProvider { defaultValue: defaultValue, evaluationContext: context ) { provider in - return provider.getDoubleEvaluation(key:defaultValue:context:) + provider.getDoubleEvaluation(key:defaultValue:context:) } } @@ -109,7 +109,7 @@ public class MultiProvider: FeatureProvider { defaultValue: defaultValue, evaluationContext: context ) { provider in - return provider.getObjectEvaluation(key:defaultValue:context:) + provider.getObjectEvaluation(key:defaultValue:context:) } } @@ -119,11 +119,12 @@ public class MultiProvider: FeatureProvider { public struct MultiProviderMetadata: ProviderMetadata { public var name: String? - + init(providers: [FeatureProvider]) { - self.name = providers.map({ + name = providers.map { $0.metadata.name ?? "MultiProvider" - }).joined(separator: ", ") + } + .joined(separator: ", ") } } } diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index 39feec9..eb14b04 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -201,22 +201,19 @@ final class DeveloperExperienceTests: XCTestCase { getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, observe: { mockEvent2Subject.eraseToAnyPublisher() } ) - // Create MultiProvider with both providers let multiProvider = MultiProvider(providers: [eventEmittingProvider1, eventEmittingProvider2]) - // Set up expectations for different events let readyExpectation = XCTestExpectation(description: "Ready event received") let configChangedExpectation = XCTestExpectation(description: "Configuration changed event received") let errorExpectation = XCTestExpectation(description: "Error event received") - + var receivedEvents: [ProviderEvent] = [] - // Observe events from MultiProvider let observer = multiProvider.observe().sink { event in guard let event = event else { return } receivedEvents.append(event) - + switch event { case .ready: readyExpectation.fulfill() @@ -228,26 +225,25 @@ final class DeveloperExperienceTests: XCTestCase { break } } - + // Set the MultiProvider in OpenFeatureAPI to test integration await OpenFeatureAPI.shared.setProviderAndWait(provider: multiProvider) - + // Emit events from the first provider mockEvent1Subject.send(.ready) mockEvent1Subject.send(.configurationChanged) - + // Emit events from the second provider mockEvent2Subject.send(.error(errorCode: .general, message: "Test error")) - // Wait for all events to be received await fulfillment(of: [readyExpectation, configChangedExpectation, errorExpectation], timeout: 2) - + // Verify that events from both providers were received XCTAssertTrue(receivedEvents.contains(.ready)) XCTAssertTrue(receivedEvents.contains(.configurationChanged)) XCTAssertTrue(receivedEvents.contains(.error(errorCode: .general, message: "Test error"))) XCTAssertGreaterThanOrEqual(receivedEvents.count, 3) - + observer.cancel() } } From cf1baae7c71eae5deb0305239347d3b8419dd929 Mon Sep 17 00:00:00 2001 From: jescriba Date: Wed, 10 Sep 2025 18:05:28 -0700 Subject: [PATCH 11/15] Update README for multiprovider Signed-off-by: jescriba --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 539e7a3..f1e0be7 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Task { | ✅ | [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. | +| ✅ | [MultiProvider](#multiprovider) | Utilize multiple providers in a single application. | | ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | | ❌ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | | ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | @@ -184,6 +184,89 @@ Logging customization is not yet available in the iOS SDK. Support for named clients is not yet available in the iOS SDK. +### MultiProvider + +The `MultiProvider` allows you to combine multiple feature flag providers into a single provider, enabling you to use different providers for different flags or implement fallback mechanisms. This is useful when migrating between providers, implementing A/B testing across providers, or ensuring high availability. + +#### Basic Usage + +```swift +import OpenFeature + +Task { + // Create individual providers + let primaryProvider = PrimaryProvider() + let fallbackProvider = FallbackProvider() + + // Create a MultiProvider with default FirstFoundStrategy + let multiProvider = MultiProvider(providers: [primaryProvider, fallbackProvider]) + + // Set the MultiProvider as the global provider + await OpenFeatureAPI.shared.setProviderAndWait(provider: multiProvider) + + // Use flags normally - the MultiProvider will handle provider selection + let client = OpenFeatureAPI.shared.getClient() + let flagValue = client.getBooleanValue(key: "my-flag", defaultValue: false) +} +``` + +#### Evaluation Strategies + +The `MultiProvider` supports different strategies for evaluating flags across multiple providers: + +##### FirstFoundStrategy (Default) + +The `FirstFoundStrategy` evaluates providers in order and returns the first result that doesn't indicate "flag not found". If a provider returns an error other than "flag not found", that error is returned immediately. + +```swift +let multiProvider = MultiProvider( + providers: [primaryProvider, fallbackProvider], + strategy: FirstFoundStrategy() +) +``` + +##### FirstSuccessfulStrategy + +The `FirstSuccessfulStrategy` evaluates providers in order and returns the first successful result (no error). Unlike `FirstFoundStrategy`, it continues to the next provider if any error occurs, including "flag not found". + +```swift +let multiProvider = MultiProvider( + providers: [primaryProvider, fallbackProvider], + strategy: FirstSuccessfulStrategy() +) +``` + +#### Use Cases + +**Provider Migration:** +```swift +// Gradually migrate from OldProvider to NewProvider +let multiProvider = MultiProvider(providers: [ + NewProvider(), // Check new provider first + OldProvider() // Fall back to old provider +]) +``` + +**High Availability:** +```swift +// Use multiple providers for redundancy +let multiProvider = MultiProvider(providers: [ + RemoteProvider(), + LocalCacheProvider(), + StaticProvider() +]) +``` + +**Environment-Specific Providers:** +```swift +// Different providers for different environments +let providers = [ + EnvironmentProvider(environment: "production"), + DefaultProvider() +] +let multiProvider = MultiProvider(providers: providers) +``` + ### Eventing Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions. From b94c311eb420963bbeb560dd9d9e47986c864050 Mon Sep 17 00:00:00 2001 From: jescriba Date: Thu, 11 Sep 2025 09:21:40 -0700 Subject: [PATCH 12/15] Update name back to FirstMatchStratey Signed-off-by: jescriba --- README.md | 6 +++--- ...tSuccessfulStrategy.swift => FirstMatchStrategy.swift} | 4 ++-- .../Provider/MultiProvider/MultiProvider.swift | 4 ++-- Tests/OpenFeatureTests/MultiProviderTests.swift | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) rename Sources/OpenFeature/Provider/MultiProvider/{FirstSuccessfulStrategy.swift => FirstMatchStrategy.swift} (86%) diff --git a/README.md b/README.md index f1e0be7..f29d80d 100644 --- a/README.md +++ b/README.md @@ -225,14 +225,14 @@ let multiProvider = MultiProvider( ) ``` -##### FirstSuccessfulStrategy +##### FirstMatchStrategy -The `FirstSuccessfulStrategy` evaluates providers in order and returns the first successful result (no error). Unlike `FirstFoundStrategy`, it continues to the next provider if any error occurs, including "flag not found". +The `FirstMatchStrategy` evaluates providers in order and returns the first successful result (no error). Unlike `FirstFoundStrategy`, it continues to the next provider if any error occurs, including "flag not found". ```swift let multiProvider = MultiProvider( providers: [primaryProvider, fallbackProvider], - strategy: FirstSuccessfulStrategy() + strategy: FirstMatchStrategy() ) ``` diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift similarity index 86% rename from Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift rename to Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift index 3897926..9fdafe8 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -1,7 +1,7 @@ -/// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers +/// FirstMatchStrategy is a strategy that evaluates a feature flag across multiple providers /// and returns the first result. Similar to `FirstFoundStrategy` but does not bubble up individual provider errors. /// If no provider successfully responds, it will return an error. -final public class FirstSuccessfulStrategy: Strategy { +final public class FirstMatchStrategy: Strategy { public func evaluate( providers: [FeatureProvider], key: String, diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift index b3ae466..c2c74d1 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -121,8 +121,8 @@ public class MultiProvider: FeatureProvider { public var name: String? init(providers: [FeatureProvider]) { - name = providers.map { - $0.metadata.name ?? "MultiProvider" + name = "MultiProvider: " + providers.map { + $0.metadata.name ?? "Provider" } .joined(separator: ", ") } diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 4e01a6e..39a001c 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -174,7 +174,7 @@ final class MultiProviderTests: XCTestCase { } } - func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_HandlesError() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesError() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( @@ -191,7 +191,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstSuccessfulStrategy() + strategy: FirstMatchStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -200,7 +200,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertNil(boolResult.errorCode) } - func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_MissingFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_MissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -216,7 +216,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstSuccessfulStrategy() + strategy: FirstMatchStrategy() ) let defaultValue = false From 9d4f798a44169d24f68ed28e1caacec8d4397b05 Mon Sep 17 00:00:00 2001 From: jescriba Date: Thu, 11 Sep 2025 09:28:26 -0700 Subject: [PATCH 13/15] Fixes renaming with proper name for FirstMatchStrategy and FirstSuccessfulStrategy Signed-off-by: jescriba --- README.md | 14 +++---- .../MultiProvider/FirstFoundStrategy.swift | 40 ------------------- .../MultiProvider/FirstMatchStrategy.swift | 19 +++++++-- .../FirstSuccessfulStrategy.swift | 29 ++++++++++++++ .../MultiProvider/MultiProvider.swift | 4 +- .../OpenFeatureTests/MultiProviderTests.swift | 28 ++++++------- 6 files changed, 67 insertions(+), 67 deletions(-) delete mode 100644 Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift create mode 100644 Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift diff --git a/README.md b/README.md index f29d80d..1d161cb 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Task { let primaryProvider = PrimaryProvider() let fallbackProvider = FallbackProvider() - // Create a MultiProvider with default FirstFoundStrategy + // Create a MultiProvider with default FirstMatchStrategy let multiProvider = MultiProvider(providers: [primaryProvider, fallbackProvider]) // Set the MultiProvider as the global provider @@ -214,25 +214,25 @@ Task { The `MultiProvider` supports different strategies for evaluating flags across multiple providers: -##### FirstFoundStrategy (Default) +##### FirstMatchStrategy (Default) -The `FirstFoundStrategy` evaluates providers in order and returns the first result that doesn't indicate "flag not found". If a provider returns an error other than "flag not found", that error is returned immediately. +The `FirstMatchStrategy` evaluates providers in order and returns the first result that doesn't indicate "flag not found". If a provider returns an error other than "flag not found", that error is returned immediately. ```swift let multiProvider = MultiProvider( providers: [primaryProvider, fallbackProvider], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) ``` -##### FirstMatchStrategy +##### FirstSuccessfulStrategy -The `FirstMatchStrategy` evaluates providers in order and returns the first successful result (no error). Unlike `FirstFoundStrategy`, it continues to the next provider if any error occurs, including "flag not found". +The `FirstSuccessfulStrategy` evaluates providers in order and returns the first successful result (no error). Unlike `FirstMatchStrategy`, it continues to the next provider if any error occurs, including "flag not found". ```swift let multiProvider = MultiProvider( providers: [primaryProvider, fallbackProvider], - strategy: FirstMatchStrategy() + strategy: FirstSuccessfulStrategy() ) ``` diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift deleted file mode 100644 index 4f0578e..0000000 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstFoundStrategy.swift +++ /dev/null @@ -1,40 +0,0 @@ -/// FirstFoundStrategy (FirstMatchStrategy) is a strategy that evaluates a feature flag across multiple providers -/// and returns the first result. Skips providers that indicate they had no value due to flag not found. -/// If any provider returns an error result other than flag not found, the error is returned. -final public class FirstFoundStrategy: Strategy { - public init() {} - - public func evaluate( - providers: [FeatureProvider], - key: String, - defaultValue: T, - evaluationContext: EvaluationContext?, - flagEvaluation: FlagEvaluation - ) throws -> ProviderEvaluation where T: AllowedFlagValueType { - for provider in providers { - do { - let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) - if eval.errorCode != ErrorCode.flagNotFound { - return eval - } - } catch OpenFeatureError.flagNotFoundError { - continue - } catch let error as OpenFeatureError { - return ProviderEvaluation( - value: defaultValue, - reason: Reason.error.rawValue, - errorCode: error.errorCode(), - errorMessage: error.description - ) - } catch { - throw error - } - } - - return ProviderEvaluation( - value: defaultValue, - reason: Reason.defaultReason.rawValue, - errorCode: ErrorCode.flagNotFound - ) - } -} diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift index 9fdafe8..f860c7e 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -1,7 +1,9 @@ /// FirstMatchStrategy is a strategy that evaluates a feature flag across multiple providers -/// and returns the first result. Similar to `FirstFoundStrategy` but does not bubble up individual provider errors. -/// If no provider successfully responds, it will return an error. +/// and returns the first result. Skips providers that indicate they had no value due to flag not found. +/// If any provider returns an error result other than flag not found, the error is returned. final public class FirstMatchStrategy: Strategy { + public init() {} + public func evaluate( providers: [FeatureProvider], key: String, @@ -12,11 +14,20 @@ final public class FirstMatchStrategy: Strategy { for provider in providers { do { let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) - if eval.errorCode == nil { + if eval.errorCode != ErrorCode.flagNotFound { return eval } - } catch { + } catch OpenFeatureError.flagNotFoundError { continue + } catch let error as OpenFeatureError { + return ProviderEvaluation( + value: defaultValue, + reason: Reason.error.rawValue, + errorCode: error.errorCode(), + errorMessage: error.description + ) + } catch { + throw error } } diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift new file mode 100644 index 0000000..2784d02 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -0,0 +1,29 @@ +/// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers +/// and returns the first result. Similar to `FirstMatchStrategy` but does not bubble up individual provider errors. +/// If no provider successfully responds, it will return an error. +final public class FirstSuccessfulStrategy: Strategy { + public func evaluate( + providers: [FeatureProvider], + key: String, + defaultValue: T, + evaluationContext: EvaluationContext?, + flagEvaluation: FlagEvaluation + ) throws -> ProviderEvaluation where T: AllowedFlagValueType { + for provider in providers { + do { + let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) + if eval.errorCode == nil { + return eval + } + } catch { + continue + } + } + + return ProviderEvaluation( + value: defaultValue, + reason: Reason.defaultReason.rawValue, + errorCode: ErrorCode.flagNotFound + ) + } +} diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift index c2c74d1..a80d3c2 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -16,10 +16,10 @@ public class MultiProvider: FeatureProvider { /// Initialize a MultiProvider with a list of providers and a strategy. /// - Parameters: /// - providers: A list of providers to evaluate. - /// - strategy: A strategy to evaluate the providers. Defaults to FirstFoundStrategy. + /// - strategy: A strategy to evaluate the providers. Defaults to FirstMatchStrategy. public init( providers: [FeatureProvider], - strategy: Strategy = FirstFoundStrategy() + strategy: Strategy = FirstMatchStrategy() ) { self.providers = providers self.strategy = strategy diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 39a001c..41b2595 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -45,7 +45,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(objectResult.value, mockProviderObjectValue) } - func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_FirstProviderHasFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FirstProviderHasFlag() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( @@ -58,7 +58,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -66,7 +66,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(boolResult.value, mockProvider1Value) } - func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_FlagNotFound() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_FlagNotFound() throws { let mockKey = "test-key" let mockProviderValue = true let mockProvider1 = MockProvider( @@ -87,7 +87,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -95,7 +95,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertEqual(boolResult.value, mockProviderValue) } - func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_AllProvidersMissingFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_AllProvidersMissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -109,7 +109,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) let result = try multiProvider.getBooleanEvaluation( @@ -120,7 +120,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertTrue(result.errorCode == .flagNotFound) } - func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_HandlesOpenFeatureError() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesOpenFeatureError() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -136,7 +136,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) let defaultValue = false let result = try multiProvider.getBooleanEvaluation( @@ -145,7 +145,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertNotNil(result.errorCode) } - func testEvaluationWithMultipleProvidersAndFirstFoundStrategy_Throws() throws { + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_Throws() throws { let mockKey = "test-key" let mockError = MockProvider.MockProviderError.message("test non-open feature error") let mockProvider1 = MockProvider( @@ -162,7 +162,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstFoundStrategy() + strategy: FirstMatchStrategy() ) let defaultValue = false do { @@ -174,7 +174,7 @@ final class MultiProviderTests: XCTestCase { } } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesError() throws { + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_HandlesError() throws { let mockKey = "test-key" let mockProvider1Value = true let mockProvider1 = MockProvider( @@ -191,7 +191,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstSuccessfulStrategy() ) let boolResult = try multiProvider.getBooleanEvaluation( @@ -200,7 +200,7 @@ final class MultiProviderTests: XCTestCase { XCTAssertNil(boolResult.errorCode) } - func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_MissingFlag() throws { + func testEvaluationWithMultipleProvidersAndFirstSuccessfulStrategy_MissingFlag() throws { let mockKey = "test-key" let mockProvider1 = MockProvider( initialize: { _ in }, @@ -216,7 +216,7 @@ final class MultiProviderTests: XCTestCase { ) let multiProvider = MultiProvider( providers: [mockProvider1, mockProvider2], - strategy: FirstMatchStrategy() + strategy: FirstSuccessfulStrategy() ) let defaultValue = false From 68cb15677f2178c77fba237fdb7c51af4b9eaf5a Mon Sep 17 00:00:00 2001 From: jescriba Date: Thu, 11 Sep 2025 09:51:48 -0700 Subject: [PATCH 14/15] Update FirstSuccessStrategy error code depending on provider error Signed-off-by: jescriba --- .../Provider/MultiProvider/FirstSuccessfulStrategy.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift index 2784d02..bf61ee2 100644 --- a/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -9,21 +9,27 @@ final public class FirstSuccessfulStrategy: Strategy { evaluationContext: EvaluationContext?, flagEvaluation: FlagEvaluation ) throws -> ProviderEvaluation where T: AllowedFlagValueType { + var flagNotFound = false for provider in providers { do { let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext) if eval.errorCode == nil { return eval + } else if eval.errorCode == ErrorCode.flagNotFound { + flagNotFound = true } + } catch OpenFeatureError.flagNotFoundError { + flagNotFound = true } catch { continue } } + let errorCode = flagNotFound ? ErrorCode.flagNotFound : ErrorCode.general return ProviderEvaluation( value: defaultValue, reason: Reason.defaultReason.rawValue, - errorCode: ErrorCode.flagNotFound + errorCode: errorCode ) } } From 40f040eef3199057623e990648aaaf189ba41e62 Mon Sep 17 00:00:00 2001 From: jescriba Date: Fri, 12 Sep 2025 10:07:57 -0700 Subject: [PATCH 15/15] Resolve provider tests when merging in event details work Signed-off-by: jescriba --- .../DeveloperExperienceTests.swift | 16 ++++++++-------- Tests/OpenFeatureTests/MultiProviderTests.swift | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index eb14b04..15c8cf3 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -192,12 +192,12 @@ final class DeveloperExperienceTests: XCTestCase { let mockEvent2Subject = CurrentValueSubject(nil) // Create test providers that can emit events let eventEmittingProvider1 = MockProvider( - initialize: { _ in mockEvent1Subject.send(.ready) }, + initialize: { _ in mockEvent1Subject.send(.ready(nil)) }, getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, observe: { mockEvent1Subject.eraseToAnyPublisher() } ) let eventEmittingProvider2 = MockProvider( - initialize: { _ in mockEvent2Subject.send(.ready) }, + initialize: { _ in mockEvent2Subject.send(.ready(nil)) }, getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, observe: { mockEvent2Subject.eraseToAnyPublisher() } ) @@ -230,18 +230,18 @@ final class DeveloperExperienceTests: XCTestCase { await OpenFeatureAPI.shared.setProviderAndWait(provider: multiProvider) // Emit events from the first provider - mockEvent1Subject.send(.ready) - mockEvent1Subject.send(.configurationChanged) + mockEvent1Subject.send(.ready(nil)) + mockEvent1Subject.send(.configurationChanged(nil)) // Emit events from the second provider - mockEvent2Subject.send(.error(errorCode: .general, message: "Test error")) + mockEvent2Subject.send(.error(ProviderEventDetails(message: "Test error", errorCode: .general))) // Wait for all events to be received await fulfillment(of: [readyExpectation, configChangedExpectation, errorExpectation], timeout: 2) // Verify that events from both providers were received - XCTAssertTrue(receivedEvents.contains(.ready)) - XCTAssertTrue(receivedEvents.contains(.configurationChanged)) - XCTAssertTrue(receivedEvents.contains(.error(errorCode: .general, message: "Test error"))) + XCTAssertTrue(receivedEvents.contains(.ready(nil))) + XCTAssertTrue(receivedEvents.contains(.configurationChanged(nil))) + XCTAssertTrue(receivedEvents.contains(.error(ProviderEventDetails(message: "Test error", errorCode: .general)))) XCTAssertGreaterThanOrEqual(receivedEvents.count, 3) observer.cancel() diff --git a/Tests/OpenFeatureTests/MultiProviderTests.swift b/Tests/OpenFeatureTests/MultiProviderTests.swift index 41b2595..5644d1e 100644 --- a/Tests/OpenFeatureTests/MultiProviderTests.swift +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -227,12 +227,12 @@ final class MultiProviderTests: XCTestCase { } func testObserveWithMultipleProviders() { - let mockEvent1 = ProviderEvent.ready + let mockEvent1 = ProviderEvent.ready(nil) let mockProvider1 = MockProvider( getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, observe: { Just(mockEvent1).eraseToAnyPublisher() } ) - let mockEvent2 = ProviderEvent.contextChanged + let mockEvent2 = ProviderEvent.contextChanged(nil) let mockProvider2 = MockProvider( getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, observe: { Just(mockEvent2).eraseToAnyPublisher() }