Skip to content

Commit e2d33ad

Browse files
committed
Add MultiProvider
Signed-off-by: jescriba <[email protected]>
1 parent 28ccd3e commit e2d33ad

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// FirstMatchStrategy is a strategy that evaluates a feature flag across multiple providers
2+
/// and returns the first result. Skips providers that indicate they had no value due to flag not found.
3+
/// If any provider returns an error result other than flag not found, the error is returned.
4+
final public class FirstMatchStrategy: Strategy {
5+
6+
public init() {}
7+
8+
public func evaluate<T>(
9+
providers: [FeatureProvider],
10+
key: String,
11+
defaultValue: T,
12+
evaluationContext: EvaluationContext?,
13+
flagEvaluation: FlagEvaluation<T>
14+
) throws -> ProviderEvaluation<T> where T: AllowedFlagValueType {
15+
for provider in providers {
16+
do {
17+
let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext)
18+
if eval.errorCode != ErrorCode.flagNotFound {
19+
return eval
20+
}
21+
} catch OpenFeatureError.flagNotFoundError {
22+
continue
23+
} catch {
24+
throw error
25+
}
26+
}
27+
28+
return ProviderEvaluation(
29+
value: defaultValue,
30+
reason: Reason.defaultReason.rawValue,
31+
errorCode: ErrorCode.flagNotFound
32+
)
33+
}
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// FirstSuccessfulStrategy is a strategy that evaluates a feature flag across multiple providers
2+
/// and returns the first result. Similar to `FirstMatchStrategy` but does not bubble up individual provider errors.
3+
/// If no provider successfully responds, it will throw an error.
4+
final public class FirstSuccessfulStrategy: Strategy {
5+
public func evaluate<T>(
6+
providers: [FeatureProvider],
7+
key: String,
8+
defaultValue: T,
9+
evaluationContext: EvaluationContext?,
10+
flagEvaluation: FlagEvaluation<T>
11+
) throws -> ProviderEvaluation<T> where T: AllowedFlagValueType {
12+
for provider in providers {
13+
do {
14+
let eval = try flagEvaluation(provider)(key, defaultValue, evaluationContext)
15+
if eval.errorCode == nil {
16+
return eval
17+
}
18+
} catch {
19+
continue
20+
}
21+
}
22+
23+
throw OpenFeatureError.generalError(
24+
message: "No provider returned a successful evaluation for the requested flag.")
25+
}
26+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Combine
2+
import Foundation
3+
4+
/// A provider that combines multiple providers into a single provider.
5+
public class MultiProvider: FeatureProvider {
6+
7+
public var hooks: [any Hook] {
8+
[]
9+
}
10+
11+
public static let name = "MultiProvider"
12+
public var metadata: ProviderMetadata = MultiProvider.MultiProviderMetadata()
13+
14+
private let providers: [FeatureProvider]
15+
private let strategy: Strategy
16+
17+
/// Initialize a MultiProvider with a list of providers and a strategy.
18+
/// - Parameters:
19+
/// - providers: A list of providers to evaluate.
20+
/// - strategy: A strategy to evaluate the providers. Defaults to FirstMatchStrategy.
21+
public init(
22+
providers: [FeatureProvider],
23+
strategy: Strategy = FirstMatchStrategy()
24+
) {
25+
self.providers = providers
26+
self.strategy = strategy
27+
}
28+
29+
public func initialize(initialContext: EvaluationContext?) async throws {
30+
try await withThrowingTaskGroup(of: Void.self) { group in
31+
for provider in providers {
32+
group.addTask {
33+
try await provider.initialize(initialContext: initialContext)
34+
}
35+
}
36+
try await group.waitForAll()
37+
}
38+
}
39+
40+
public func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) async throws {
41+
try await withThrowingTaskGroup(of: Void.self) { group in
42+
for provider in providers {
43+
group.addTask {
44+
try await provider.onContextSet(oldContext: oldContext, newContext: newContext)
45+
}
46+
}
47+
try await group.waitForAll()
48+
}
49+
}
50+
51+
public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
52+
-> ProviderEvaluation<Bool>
53+
{
54+
return try strategy.evaluate(
55+
providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context,
56+
flagEvaluation: { provider in
57+
return provider.getBooleanEvaluation(key:defaultValue:context:)
58+
})
59+
}
60+
61+
public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
62+
-> ProviderEvaluation<String>
63+
{
64+
return try strategy.evaluate(
65+
providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context,
66+
flagEvaluation: { provider in
67+
return provider.getStringEvaluation(key:defaultValue:context:)
68+
})
69+
}
70+
71+
public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
72+
-> ProviderEvaluation<Int64>
73+
{
74+
return try strategy.evaluate(
75+
providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context,
76+
flagEvaluation: { provider in
77+
return provider.getIntegerEvaluation(key:defaultValue:context:)
78+
})
79+
}
80+
81+
public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
82+
-> ProviderEvaluation<Double>
83+
{
84+
return try strategy.evaluate(
85+
providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context,
86+
flagEvaluation: { provider in
87+
return provider.getDoubleEvaluation(key:defaultValue:context:)
88+
})
89+
}
90+
91+
public func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
92+
-> ProviderEvaluation<Value>
93+
{
94+
return try strategy.evaluate(
95+
providers: providers, key: key, defaultValue: defaultValue, evaluationContext: context,
96+
flagEvaluation: { provider in
97+
return provider.getObjectEvaluation(key:defaultValue:context:)
98+
})
99+
}
100+
101+
public func observe() -> AnyPublisher<ProviderEvent?, Never> {
102+
return Publishers.MergeMany(providers.map { $0.observe() }).eraseToAnyPublisher()
103+
}
104+
105+
public struct MultiProviderMetadata: ProviderMetadata {
106+
public var name: String? = MultiProvider.name
107+
}
108+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// FlagEvaluation is a function that evaluates a feature flag and returns a ProviderEvaluation.
2+
/// It is used to evaluate a feature flag across multiple providers using the strategy's logic.
3+
public typealias FlagEvaluation<T> = (FeatureProvider) -> (
4+
_ key: String, _ defaultValue: T, _ evaluationContext: EvaluationContext?
5+
) throws -> ProviderEvaluation<T> where T: AllowedFlagValueType
6+
7+
/// Strategy interface defines how multiple feature providers should be evaluated
8+
/// to determine the final result for a feature flag evaluation.
9+
/// Different strategies can implement different logic for combining or selecting
10+
/// results from multiple providers.
11+
public protocol Strategy {
12+
func evaluate<T>(
13+
providers: [FeatureProvider],
14+
key: String,
15+
defaultValue: T,
16+
evaluationContext: EvaluationContext?,
17+
flagEvaluation: FlagEvaluation<T>
18+
) throws -> ProviderEvaluation<T> where T: AllowedFlagValueType
19+
}

0 commit comments

Comments
 (0)