Skip to content

Commit 7256a3c

Browse files
authored
feat!: Add support for Flag Metadata (#43)
## This PR Adds support for Flag metadata as part of the evaluation details (both from the API perspective but also from the `ProviderEvaluation`). Consider this a breaking change for Provider implementations. ### Related Issues Fixes #39 ### Notes Some formatting and import ordering slipped in as well. Signed-off-by: Nicklas Lundin <[email protected]>
1 parent 09d2871 commit 7256a3c

11 files changed

+169
-28
lines changed

Sources/OpenFeature/EventHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Foundation
21
import Combine
2+
import Foundation
33

44
public class EventHandler: EventSender, EventPublisher {
55
private let eventState: CurrentValueSubject<ProviderEvent, Never>

Sources/OpenFeature/FlagEvaluationDetails.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ public struct FlagEvaluationDetails<T: Equatable>: BaseEvaluation, Equatable {
66
public var value: T
77
public var variant: String?
88
public var reason: String?
9+
public var flagMetadata: [String: FlagMetadataValue]
910
public var errorCode: ErrorCode?
1011
public var errorMessage: String?
1112

1213
public init(
1314
flagKey: String,
1415
value: T,
16+
flagMetadata: [String: FlagMetadataValue] = [:],
1517
variant: String? = nil,
1618
reason: String? = nil,
1719
errorCode: ErrorCode? = nil,
@@ -21,6 +23,7 @@ public struct FlagEvaluationDetails<T: Equatable>: BaseEvaluation, Equatable {
2123
self.value = value
2224
self.variant = variant
2325
self.reason = reason
26+
self.flagMetadata = flagMetadata
2427
self.errorCode = errorCode
2528
self.errorMessage = errorMessage
2629
}
@@ -29,6 +32,7 @@ public struct FlagEvaluationDetails<T: Equatable>: BaseEvaluation, Equatable {
2932
return FlagEvaluationDetails(
3033
flagKey: flagKey,
3134
value: providerEval.value,
35+
flagMetadata: providerEval.flagMetadata,
3236
variant: providerEval.variant,
3337
reason: providerEval.reason,
3438
errorCode: providerEval.errorCode,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import Foundation
2+
3+
public enum FlagMetadataValue: Equatable, Codable {
4+
case boolean(Bool)
5+
case string(String)
6+
case integer(Int64)
7+
case double(Double)
8+
9+
public static func of<T>(_ value: T) -> FlagMetadataValue? {
10+
if let value = value as? Bool {
11+
return .boolean(value)
12+
} else if let value = value as? String {
13+
return .string(value)
14+
} else if let value = value as? Int64 {
15+
return .integer(value)
16+
} else if let value = value as? Double {
17+
return .double(value)
18+
} else {
19+
return nil
20+
}
21+
}
22+
23+
public func getTyped<T>() -> T? {
24+
if let value = self as? T {
25+
return value
26+
}
27+
28+
switch self {
29+
case .boolean(let value): return value as? T
30+
case .string(let value): return value as? T
31+
case .integer(let value): return value as? T
32+
case .double(let value): return value as? T
33+
}
34+
}
35+
36+
public func asBoolean() -> Bool? {
37+
if case let .boolean(bool) = self {
38+
return bool
39+
}
40+
41+
return nil
42+
}
43+
44+
public func asString() -> String? {
45+
if case let .string(string) = self {
46+
return string
47+
}
48+
49+
return nil
50+
}
51+
52+
public func asInteger() -> Int64? {
53+
if case let .integer(int64) = self {
54+
return int64
55+
}
56+
57+
return nil
58+
}
59+
60+
public func asDouble() -> Double? {
61+
if case let .double(double) = self {
62+
return double
63+
}
64+
65+
return nil
66+
}
67+
}
68+
69+
extension FlagMetadataValue: CustomStringConvertible {
70+
public var description: String {
71+
switch self {
72+
case .boolean(let value):
73+
return "\(value)"
74+
case .string(let value):
75+
return value
76+
case .integer(let value):
77+
return "\(value)"
78+
case .double(let value):
79+
return "\(value)"
80+
}
81+
}
82+
}
83+
84+
extension FlagMetadataValue {
85+
public func decode<T: Decodable>() throws -> T {
86+
let data = try JSONSerialization.data(withJSONObject: toJson(value: self))
87+
return try JSONDecoder().decode(T.self, from: data)
88+
}
89+
90+
func toJson(value: FlagMetadataValue) -> Any {
91+
switch value {
92+
case .boolean(let bool):
93+
return bool
94+
case .string(let string):
95+
return string
96+
case .integer(let int64):
97+
return int64
98+
case .double(let double):
99+
return double
100+
}
101+
}
102+
}

Sources/OpenFeature/OpenFeatureAPI.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Foundation
21
import Combine
2+
import Foundation
33

44
/// A global singleton which holds base configuration for the OpenFeature library.
55
/// Configuration here will be shared across all ``Client``s.

Sources/OpenFeature/Provider/NoOpProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Foundation
21
import Combine
2+
import Foundation
33

44
/// A ``FeatureProvider`` that simply returns the default values passed to it.
55
class NoOpProvider: FeatureProvider {

Sources/OpenFeature/Provider/ProviderEvaluation.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@ import Foundation
22

33
public struct ProviderEvaluation<T> {
44
public var value: T
5+
public var flagMetadata: [String: FlagMetadataValue]
56
public var variant: String?
67
public var reason: String?
78
public var errorCode: ErrorCode?
89
public var errorMessage: String?
910

1011
public init(
1112
value: T,
13+
flagMetadata: [String: FlagMetadataValue] = [:],
1214
variant: String? = nil,
1315
reason: String? = nil,
1416
errorCode: ErrorCode? = nil,
1517
errorMessage: String? = nil
1618
) {
1719
self.value = value
1820
self.variant = variant
21+
self.flagMetadata = flagMetadata
1922
self.reason = reason
2023
self.errorCode = errorCode
2124
self.errorMessage = errorMessage

Tests/OpenFeatureTests/DeveloperExperienceTests.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,18 @@ final class DeveloperExperienceTests: XCTestCase {
5252
let errorExpectation = XCTestExpectation(description: "Error")
5353
withExtendedLifetime(
5454
OpenFeatureAPI.shared.observe().sink { event in
55-
switch event {
56-
case .notReady:
57-
notReadyExpectation.fulfill()
58-
case .ready:
59-
readyExpectation.fulfill()
60-
case .error:
61-
errorExpectation.fulfill()
62-
default:
63-
XCTFail("Unexpected event")
55+
switch event {
56+
case .notReady:
57+
notReadyExpectation.fulfill()
58+
case .ready:
59+
readyExpectation.fulfill()
60+
case .error:
61+
errorExpectation.fulfill()
62+
default:
63+
XCTFail("Unexpected event")
64+
}
6465
}
65-
})
66-
{
66+
) {
6767
let initCompleteExpectation = XCTestExpectation()
6868

6969
let eventHandler = EventHandler()

Tests/OpenFeatureTests/FlagEvaluationTests.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import Combine
12
import Foundation
23
import XCTest
3-
import Combine
44

55
@testable import OpenFeature
66

@@ -129,35 +129,40 @@ final class FlagEvaluationTests: XCTestCase {
129129
wait(for: [readyExpectation], timeout: 5)
130130
let client = OpenFeatureAPI.shared.getClient()
131131
let key = "key"
132-
let booleanDetails = FlagEvaluationDetails(flagKey: key, value: true, variant: nil)
132+
let booleanDetails = FlagEvaluationDetails(
133+
flagKey: key, value: true, flagMetadata: DoSomethingProvider.flagMetadataMap, variant: nil)
133134
XCTAssertEqual(client.getDetails(key: key, defaultValue: false), booleanDetails)
134135
XCTAssertEqual(client.getDetails(key: key, defaultValue: false), booleanDetails)
135136
XCTAssertEqual(
136137
client.getDetails(
137138
key: key, defaultValue: false, options: FlagEvaluationOptions()), booleanDetails)
138139

139-
let stringDetails = FlagEvaluationDetails(flagKey: key, value: "tset", variant: nil)
140+
let stringDetails = FlagEvaluationDetails(
141+
flagKey: key, value: "tset", flagMetadata: DoSomethingProvider.flagMetadataMap, variant: nil)
140142
XCTAssertEqual(client.getDetails(key: key, defaultValue: "test"), stringDetails)
141143
XCTAssertEqual(client.getDetails(key: key, defaultValue: "test"), stringDetails)
142144
XCTAssertEqual(
143145
client.getDetails(
144146
key: key, defaultValue: "test", options: FlagEvaluationOptions()), stringDetails)
145147

146-
let integerDetails = FlagEvaluationDetails(flagKey: key, value: Int64(400), variant: nil)
148+
let integerDetails = FlagEvaluationDetails(
149+
flagKey: key, value: Int64(400), flagMetadata: DoSomethingProvider.flagMetadataMap, variant: nil)
147150
XCTAssertEqual(client.getDetails(key: key, defaultValue: 4), integerDetails)
148151
XCTAssertEqual(client.getDetails(key: key, defaultValue: 4), integerDetails)
149152
XCTAssertEqual(
150153
client.getDetails(
151154
key: key, defaultValue: 4, options: FlagEvaluationOptions()), integerDetails)
152155

153-
let doubleDetails = FlagEvaluationDetails(flagKey: key, value: 40.0, variant: nil)
156+
let doubleDetails = FlagEvaluationDetails(
157+
flagKey: key, value: 40.0, flagMetadata: DoSomethingProvider.flagMetadataMap, variant: nil)
154158
XCTAssertEqual(client.getDetails(key: key, defaultValue: 0.4), doubleDetails)
155159
XCTAssertEqual(client.getDetails(key: key, defaultValue: 0.4), doubleDetails)
156160
XCTAssertEqual(
157161
client.getDetails(
158162
key: key, defaultValue: 0.4, options: FlagEvaluationOptions()), doubleDetails)
159163

160-
let objectDetails = FlagEvaluationDetails(flagKey: key, value: Value.null, variant: nil)
164+
let objectDetails = FlagEvaluationDetails(
165+
flagKey: key, value: Value.null, flagMetadata: DoSomethingProvider.flagMetadataMap, variant: nil)
161166
XCTAssertEqual(client.getDetails(key: key, defaultValue: .structure([:])), objectDetails)
162167
XCTAssertEqual(
163168
client.getDetails(key: key, defaultValue: .structure([:])), objectDetails)
@@ -272,4 +277,22 @@ final class FlagEvaluationTests: XCTestCase {
272277
let client = OpenFeatureAPI.shared.getClient(name: "test", version: nil)
273278
XCTAssertEqual(client.metadata.name, "test")
274279
}
280+
281+
func testFlagMetadata() {
282+
OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider())
283+
let client = OpenFeatureAPI.shared.getClient()
284+
let details = client.getDetails(key: "testkey", defaultValue: false)
285+
286+
// These test values are hard coded for all evaluations from the DoSomethingProvider
287+
XCTAssertEqual(details.flagMetadata["int-metadata"]?.asInteger(), 99)
288+
XCTAssertEqual(details.flagMetadata["double-metadata"]?.asDouble(), 98.4)
289+
XCTAssertEqual(details.flagMetadata["string-metadata"]?.asString(), "hello-world")
290+
XCTAssertEqual(details.flagMetadata["boolean-metadata"]?.asBoolean(), true)
291+
292+
// Non existent key
293+
XCTAssertNil(details.flagMetadata["non-existent-key"])
294+
295+
// Invalid mapping an int to string returns nil
296+
XCTAssertNil(details.flagMetadata["int-metadata"]?.asString())
297+
}
275298
}

Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import Combine
12
import Foundation
23
import OpenFeature
3-
import Combine
44

55
class DoSomethingProvider: FeatureProvider {
66
public static let name = "Something"
@@ -23,39 +23,40 @@ class DoSomethingProvider: FeatureProvider {
2323
Bool
2424
>
2525
{
26-
return ProviderEvaluation(value: !defaultValue)
26+
return ProviderEvaluation(value: !defaultValue, flagMetadata: DoSomethingProvider.flagMetadataMap)
2727
}
2828

2929
func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
3030
-> ProviderEvaluation<
3131
String
3232
>
3333
{
34-
return ProviderEvaluation(value: String(defaultValue.reversed()))
34+
return ProviderEvaluation(
35+
value: String(defaultValue.reversed()), flagMetadata: DoSomethingProvider.flagMetadataMap)
3536
}
3637

3738
func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
3839
-> ProviderEvaluation<
3940
Int64
4041
>
4142
{
42-
return ProviderEvaluation(value: defaultValue * 100)
43+
return ProviderEvaluation(value: defaultValue * 100, flagMetadata: DoSomethingProvider.flagMetadataMap)
4344
}
4445

4546
func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
4647
-> ProviderEvaluation<
4748
Double
4849
>
4950
{
50-
return ProviderEvaluation(value: defaultValue * 100)
51+
return ProviderEvaluation(value: defaultValue * 100, flagMetadata: DoSomethingProvider.flagMetadataMap)
5152
}
5253

5354
func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
5455
-> ProviderEvaluation<
5556
Value
5657
>
5758
{
58-
return ProviderEvaluation(value: .null)
59+
return ProviderEvaluation(value: .null, flagMetadata: DoSomethingProvider.flagMetadataMap)
5960
}
6061

6162
func observe() -> AnyPublisher<ProviderEvent, Never> {
@@ -65,4 +66,11 @@ class DoSomethingProvider: FeatureProvider {
6566
public struct DoMetadata: ProviderMetadata {
6667
public var name: String? = DoSomethingProvider.name
6768
}
69+
70+
public static let flagMetadataMap = [
71+
"int-metadata": FlagMetadataValue.integer(99),
72+
"double-metadata": FlagMetadataValue.double(98.4),
73+
"string-metadata": FlagMetadataValue.string("hello-world"),
74+
"boolean-metadata": FlagMetadataValue.boolean(true),
75+
]
6876
}

Tests/OpenFeatureTests/Helpers/InjectableEventHandlerProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import Combine
12
import Foundation
23
import OpenFeature
3-
import Combine
44

55
class InjectableEventHandlerProvider: FeatureProvider {
66
public static let name = "InjectableEventHandler"

0 commit comments

Comments
 (0)