Skip to content

Commit 8bf31e2

Browse files
feat: Support Exporter Metadata (#16)
* feat: Exporter Metadata Signed-off-by: Thomas Poignant <[email protected]> * remove unused code Signed-off-by: Thomas Poignant <[email protected]> * adding test with exporter metadata nil Signed-off-by: Thomas Poignant <[email protected]> * remove unused code Signed-off-by: Thomas Poignant <[email protected]> --------- Signed-off-by: Thomas Poignant <[email protected]>
1 parent 9f9b8cc commit 8bf31e2

File tree

6 files changed

+155
-8
lines changed

6 files changed

+155
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Foundation
2+
import OpenFeature
3+
4+
// Define a Codable enum that can represent any type of JSON value
5+
public enum ExporterMetadataValue: Codable, Equatable {
6+
case string(String)
7+
case integer(Int64)
8+
case double(Double)
9+
case bool(Bool)
10+
11+
// Decode the JSON based on its type
12+
public init(from decoder: Decoder) throws {
13+
let container = try decoder.singleValueContainer()
14+
if let stringValue = try? container.decode(String.self) {
15+
self = .string(stringValue)
16+
} else if let intValue = try? container.decode(Int64.self) {
17+
self = .integer(intValue)
18+
} else if let doubleValue = try? container.decode(Double.self) {
19+
self = .double(doubleValue)
20+
} else if let boolValue = try? container.decode(Bool.self) {
21+
self = .bool(boolValue)
22+
} else {
23+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode JSONValue")
24+
}
25+
}
26+
27+
// Encode the JSON based on its type
28+
public func encode(to encoder: Encoder) throws {
29+
var container = encoder.singleValueContainer()
30+
switch self {
31+
case .string(let value):
32+
try container.encode(value)
33+
case .integer(let value):
34+
try container.encode(value)
35+
case .double(let value):
36+
try container.encode(value)
37+
case .bool(let value):
38+
try container.encode(value)
39+
}
40+
}
41+
}

Sources/GOFeatureFlag/controller/goff_api.swift

+12-4
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ import OFREP
44

55
class GoFeatureFlagAPI {
66
private let networkingService: NetworkingService
7-
private let options: GoFeatureFlagProviderOptions
8-
private let metadata: [String:String] = ["provider": "openfeature-swift"]
7+
private var options: GoFeatureFlagProviderOptions
98

109
init(networkingService: NetworkingService, options: GoFeatureFlagProviderOptions) {
1110
self.networkingService = networkingService
1211
self.options = options
12+
13+
if self.options.exporterMetadata == nil {
14+
self.options.exporterMetadata = [:]
15+
}
16+
if var exporterMetadata = self.options.exporterMetadata {
17+
exporterMetadata["openfeature"] = .bool(true)
18+
exporterMetadata["provider"] = .string("swift")
19+
self.options.exporterMetadata = exporterMetadata
20+
}
1321
}
1422

1523
func postDataCollector(events: [FeatureEvent]?) async throws -> (DataCollectorResponse, HTTPURLResponse) {
@@ -27,8 +35,8 @@ class GoFeatureFlagAPI {
2735
let dataCollectorURL = url.appendingPathComponent("v1/data/collector")
2836
var request = URLRequest(url: dataCollectorURL)
2937
request.httpMethod = "POST"
30-
31-
let requestBody = DataCollectorRequest(meta: metadata, events: events)
38+
39+
let requestBody = DataCollectorRequest(meta: self.options.exporterMetadata, events: events)
3240
let encoder = JSONEncoder()
3341
encoder.outputFormatting = .prettyPrinted
3442
request.httpBody = try encoder.encode(requestBody)

Sources/GOFeatureFlag/model/data_collector_request.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Foundation
22

33
struct DataCollectorRequest: Codable {
4-
var meta: [String:String]?
4+
var meta: [String:ExporterMetadataValue]?
55
var events: [FeatureEvent]? = []
66

7-
public init(meta: [String:String]? = [:], events: [FeatureEvent]? = []) {
7+
public init(meta: [String:ExporterMetadataValue]? = [:], events: [FeatureEvent]? = []) {
88
self.meta = meta
99
self.events = events
1010
}

Sources/GOFeatureFlag/options.swift

+7
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,24 @@ public struct GoFeatureFlagProviderOptions {
3232
* default: URLSession.shared
3333
*/
3434
public var networkService: NetworkingService?
35+
/**
36+
* (optional) exporter metadata to be sent to the relay proxy data collector to be used for evaluation data events.
37+
* default: empty
38+
*/
39+
public var exporterMetadata: [String:ExporterMetadataValue]? = [:]
3540

3641
public init(
3742
endpoint: String,
3843
pollInterval: TimeInterval = 60,
3944
apiKey: String? = nil,
4045
dataFlushInterval: TimeInterval = 600,
46+
exporterMetadata: [String:ExporterMetadataValue]? = [:],
4147
networkService: NetworkingService? = URLSession.shared) {
4248
self.endpoint = endpoint
4349
self.pollInterval = pollInterval
4450
self.apiKey = apiKey
4551
self.networkService = networkService
4652
self.dataCollectorInterval = dataFlushInterval
53+
self.exporterMetadata = exporterMetadata
4754
}
4855
}

Tests/GOFeatureFlagTests/mock_networking_service.swift

+2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ public class MockNetworkingService: NetworkingService {
1010
var callCounter = 0
1111
var dataCollectorCallCounter = 0
1212
var dataCollectorEventCounter = 0
13+
var requests: [URLRequest] = []
1314

1415
public init(mockStatus: Int = 200) {
1516
self.mockStatus = mockStatus
1617
}
1718

1819
public func doRequest(for request: URLRequest) async throws -> (Data, URLResponse) {
20+
self.requests.append(request)
1921
self.callCounter+=1
2022
let isDataCollector = request.url?.absoluteString.contains("/v1/data/collector") ?? false
2123
let isBulkEvaluation = request.url?.absoluteString.contains("/ofrep/v1/evaluate/flags") ?? false

Tests/GOFeatureFlagTests/provider_tests.swift

+91-2
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,101 @@ class GoFeatureFlagProviderTests: XCTestCase {
3434

3535
let expectation = self.expectation(description: "Waiting for delay")
3636

37-
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) { expectation.fulfill() }
38-
await fulfillment(of: [expectation], timeout: 2.0)
37+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { expectation.fulfill() }
38+
await fulfillment(of: [expectation], timeout: 3.0)
3939

4040
XCTAssertEqual(1, mockNetworkService.dataCollectorCallCounter)
4141
XCTAssertEqual(6, mockNetworkService.dataCollectorEventCounter)
4242
}
43+
44+
func testExporterMetadata() async {
45+
let mockNetworkService = MockNetworkingService(mockStatus: 200)
46+
let provider = GoFeatureFlagProvider(
47+
options: GoFeatureFlagProviderOptions(
48+
endpoint: "https://localhost:1031",
49+
dataFlushInterval: 1,
50+
exporterMetadata: ["version": ExporterMetadataValue.string("1.0.0"),"testInt": ExporterMetadataValue.integer(123), "testDouble": ExporterMetadataValue.double(123.45)],
51+
networkService: mockNetworkService
52+
)
53+
)
54+
let evaluationCtx = MutableContext(targetingKey: "ede04e44-463d-40d1-8fc0-b1d6855578d0")
55+
let api = OpenFeatureAPI()
56+
await api.setProviderAndWait(provider: provider, initialContext: evaluationCtx)
57+
let client = api.getClient()
58+
59+
_ = client.getBooleanDetails(key: "my-flag", defaultValue: false)
60+
_ = client.getBooleanDetails(key: "my-flag", defaultValue: false)
61+
_ = client.getIntegerDetails(key: "int-flag", defaultValue: 1)
62+
_ = client.getDoubleDetails(key: "double-flag", defaultValue: 1.0)
63+
_ = client.getStringDetails(key: "string-flag", defaultValue: "default")
64+
_ = client.getObjectDetails(key: "object-flag", defaultValue: Value.null)
65+
66+
let expectation = self.expectation(description: "Waiting for delay")
67+
68+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { expectation.fulfill() }
69+
await fulfillment(of: [expectation], timeout: 3.0)
70+
71+
XCTAssertEqual(1, mockNetworkService.dataCollectorCallCounter)
72+
XCTAssertEqual(6, mockNetworkService.dataCollectorEventCounter)
73+
74+
do {
75+
let httpBodyCollector = mockNetworkService.requests[mockNetworkService.requests.count - 1].httpBody!
76+
let decodedStruct = try JSONDecoder().decode(DataCollectorRequest.self, from: httpBodyCollector)
77+
let want = [
78+
"version": ExporterMetadataValue.string("1.0.0"),
79+
"testDouble": ExporterMetadataValue.double(123.45),
80+
"testInt": ExporterMetadataValue.integer(123),
81+
"openfeature": ExporterMetadataValue.bool(true),
82+
"provider": ExporterMetadataValue.string("swift")
83+
] as? [String: ExporterMetadataValue]
84+
XCTAssertEqual(want, decodedStruct.meta)
85+
} catch {
86+
XCTFail("Error deserializing: \(error)")
87+
}
88+
}
89+
90+
func testExporterMetadataNil() async {
91+
let mockNetworkService = MockNetworkingService(mockStatus: 200)
92+
let provider = GoFeatureFlagProvider(
93+
options: GoFeatureFlagProviderOptions(
94+
endpoint: "https://localhost:1031",
95+
dataFlushInterval: 1,
96+
exporterMetadata: nil,
97+
networkService: mockNetworkService
98+
)
99+
)
100+
let evaluationCtx = MutableContext(targetingKey: "ede04e44-463d-40d1-8fc0-b1d6855578d0")
101+
let api = OpenFeatureAPI()
102+
await api.setProviderAndWait(provider: provider, initialContext: evaluationCtx)
103+
let client = api.getClient()
104+
105+
_ = client.getBooleanDetails(key: "my-flag", defaultValue: false)
106+
_ = client.getBooleanDetails(key: "my-flag", defaultValue: false)
107+
_ = client.getIntegerDetails(key: "int-flag", defaultValue: 1)
108+
_ = client.getDoubleDetails(key: "double-flag", defaultValue: 1.0)
109+
_ = client.getStringDetails(key: "string-flag", defaultValue: "default")
110+
_ = client.getObjectDetails(key: "object-flag", defaultValue: Value.null)
111+
112+
let expectation = self.expectation(description: "Waiting for delay")
113+
114+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { expectation.fulfill() }
115+
await fulfillment(of: [expectation], timeout: 3.0)
116+
117+
XCTAssertEqual(1, mockNetworkService.dataCollectorCallCounter)
118+
XCTAssertEqual(6, mockNetworkService.dataCollectorEventCounter)
119+
120+
do {
121+
let httpBodyCollector = mockNetworkService.requests[mockNetworkService.requests.count - 1].httpBody!
122+
let decodedStruct = try JSONDecoder().decode(DataCollectorRequest.self, from: httpBodyCollector)
123+
let want = [
124+
"openfeature": ExporterMetadataValue.bool(true),
125+
"provider": ExporterMetadataValue.string("swift")
126+
] as? [String: ExporterMetadataValue]
127+
XCTAssertEqual(want, decodedStruct.meta)
128+
} catch {
129+
XCTFail("Error deserializing: \(error)")
130+
}
131+
}
43132

44133
func testProviderMultipleHookCall() async {
45134
let mockNetworkService = MockNetworkingService(mockStatus: 200)

0 commit comments

Comments
 (0)