diff --git a/README.md b/README.md index 539e7a3..1d161cb 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 FirstMatchStrategy + 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: + +##### FirstMatchStrategy (Default) + +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: FirstMatchStrategy() +) +``` + +##### FirstSuccessfulStrategy + +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: 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. diff --git a/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift new file mode 100644 index 0000000..f860c7e --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstMatchStrategy.swift @@ -0,0 +1,40 @@ +/// 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 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/FirstSuccessfulStrategy.swift b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift new file mode 100644 index 0000000..bf61ee2 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/FirstSuccessfulStrategy.swift @@ -0,0 +1,35 @@ +/// 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 { + 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 + ) + } +} diff --git a/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift new file mode 100644 index 0000000..a80d3c2 --- /dev/null +++ b/Sources/OpenFeature/Provider/MultiProvider/MultiProvider.swift @@ -0,0 +1,130 @@ +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 + + 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 + metadata = MultiProviderMetadata(providers: providers) + } + + 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 + ) { provider in + 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 + ) { provider in + 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 + ) { provider in + 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 + ) { provider in + 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 + ) { provider in + 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? + + init(providers: [FeatureProvider]) { + name = "MultiProvider: " + providers.map { + $0.metadata.name ?? "Provider" + } + .joined(separator: ", ") + } + } +} 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 +} diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index 1bd1b28..15c8cf3 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,64 @@ 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(nil)) }, + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { mockEvent1Subject.eraseToAnyPublisher() } + ) + let eventEmittingProvider2 = MockProvider( + initialize: { _ in mockEvent2Subject.send(.ready(nil)) }, + 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(nil)) + mockEvent1Subject.send(.configurationChanged(nil)) + + // Emit events from the second provider + 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(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 new file mode 100644 index 0000000..5644d1e --- /dev/null +++ b/Tests/OpenFeatureTests/MultiProviderTests.swift @@ -0,0 +1,317 @@ +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.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 = MultiProviderTestHelpers.mockThrowingProvider(error: mockError) + // Second provider has the flag and test using all types + 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()) + 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( + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in ProviderEvaluation(value: mockProvider1Value) } + ) + let mockProvider2 = MockProvider( + 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()) + 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) + } 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( + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + 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() + ) + XCTAssertTrue(result.errorCode == .flagNotFound) + } + + func testEvaluationWithMultipleProvidersAndFirstMatchStrategy_HandlesOpenFeatureError() throws { + let mockKey = "test-key" + let mockProvider1 = MockProvider( + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ in + throw OpenFeatureError.generalError(message: "test error") + } + ) + let multiProvider = MultiProvider( + 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: defaultValue, context: MutableContext()) + XCTFail("Expected to throw") + } catch { + XCTAssertTrue(error is MockProvider.MockProviderError) + } + } + + 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( + initialize: { _ in }, + getBooleanEvaluation: { _, _, _ 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( + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let mockProvider2 = MockProvider( + initialize: { _ in }, + getBooleanEvaluation: { _, defaultValue, _ in + return ProviderEvaluation(value: defaultValue, errorCode: .flagNotFound) + } + ) + let multiProvider = MultiProvider( + providers: [mockProvider1, mockProvider2], + strategy: FirstSuccessfulStrategy() + ) + + let defaultValue = false + let result = try multiProvider.getBooleanEvaluation( + key: mockKey, defaultValue: defaultValue, context: MutableContext()) + XCTAssertEqual(result.errorCode, .flagNotFound) + XCTAssertEqual(result.value, defaultValue) + } + + func testObserveWithMultipleProviders() { + let mockEvent1 = ProviderEvent.ready(nil) + let mockProvider1 = MockProvider( + getBooleanEvaluation: { _, _, _ in throw OpenFeatureError.generalError(message: "test error") }, + observe: { Just(mockEvent1).eraseToAnyPublisher() } + ) + let mockEvent2 = ProviderEvent.contextChanged(nil) + let mockProvider2 = MockProvider( + 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 { event in + if let event { + receivedEvents.append(event) + } + if receivedEvents.count == mockEvents.count { + fulfillment.fulfill() + } + } + wait(for: [fulfillment], timeout: 2) + observation.cancel() + XCTAssertEqual(receivedEvents.count, mockEvents.count) + XCTAssertTrue(receivedEvents.contains(mockEvent1)) + XCTAssertTrue(receivedEvents.contains(mockEvent2)) + } +} + +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) + } + ) + } +}