Skip to content

Commit e8690d1

Browse files
feat!: Add ImmutableContext implementation (#70)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR - Don't expose `MutableContext`, only use it in tests - Expose a new `ImmutableContext`, with methods to safely edit it by returning new immutable copies - Remove `deepCopy` operations, not needed with the new ImmutableContext ### Related Issues We've received crash reports on the Confidence Provider that we believe are caused by the passed context object being mutated at runtime. --------- Signed-off-by: Fabrizio Demaria <[email protected]>
1 parent 2a7cf8a commit e8690d1

File tree

6 files changed

+348
-13
lines changed

6 files changed

+348
-13
lines changed

Sources/OpenFeature/EvaluationContext.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ import Foundation
44
public protocol EvaluationContext: Structure {
55
func getTargetingKey() -> String
66
func deepCopy() -> EvaluationContext
7-
func setTargetingKey(targetingKey: String)
87
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import Foundation
2+
3+
/// The ``ImmutableContext`` is an ``EvaluationContext`` implementation which is immutable and thread-safe.
4+
/// It provides read-only access to context data and cannot be modified after creation.
5+
public struct ImmutableContext: EvaluationContext {
6+
private let targetingKey: String
7+
private let structure: ImmutableStructure
8+
9+
public init(targetingKey: String = "", structure: ImmutableStructure = ImmutableStructure()) {
10+
self.targetingKey = targetingKey
11+
self.structure = structure
12+
}
13+
14+
public init(attributes: [String: Value]) {
15+
self.init(structure: ImmutableStructure(attributes: attributes))
16+
}
17+
18+
public func deepCopy() -> EvaluationContext {
19+
return ImmutableContext(targetingKey: targetingKey, structure: structure.deepCopy())
20+
}
21+
22+
public func getTargetingKey() -> String {
23+
return targetingKey
24+
}
25+
26+
public func keySet() -> Set<String> {
27+
return structure.keySet()
28+
}
29+
30+
public func getValue(key: String) -> Value? {
31+
return structure.getValue(key: key)
32+
}
33+
34+
public func asMap() -> [String: Value] {
35+
return structure.asMap()
36+
}
37+
38+
public func asObjectMap() -> [String: AnyHashable?] {
39+
return structure.asObjectMap()
40+
}
41+
}
42+
43+
extension ImmutableContext {
44+
public func withTargetingKey(_ targetingKey: String) -> ImmutableContext {
45+
return ImmutableContext(targetingKey: targetingKey, structure: structure)
46+
}
47+
48+
public func withAttribute(key: String, value: Value) -> ImmutableContext {
49+
var newAttributes = structure.asMap()
50+
newAttributes[key] = value
51+
return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes))
52+
}
53+
54+
public func withAttributes(_ attributes: [String: Value]) -> ImmutableContext {
55+
var newAttributes = structure.asMap()
56+
for (key, value) in attributes {
57+
newAttributes[key] = value
58+
}
59+
return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes))
60+
}
61+
62+
public func withoutAttribute(key: String) -> ImmutableContext {
63+
var newAttributes = structure.asMap()
64+
newAttributes.removeValue(forKey: key)
65+
return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes))
66+
}
67+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import Foundation
2+
3+
/// The ``ImmutableStructure`` is a ``Structure`` implementation which is immutable and thread-safe.
4+
/// It provides read-only access to structured data and cannot be modified after creation.
5+
public class ImmutableStructure: Structure {
6+
private let attributes: [String: Value]
7+
8+
public init(attributes: [String: Value] = [:]) {
9+
self.attributes = attributes
10+
}
11+
12+
public func keySet() -> Set<String> {
13+
return Set(attributes.keys)
14+
}
15+
16+
public func getValue(key: String) -> Value? {
17+
return attributes[key]
18+
}
19+
20+
public func asMap() -> [String: Value] {
21+
return attributes
22+
}
23+
24+
public func asObjectMap() -> [String: AnyHashable?] {
25+
return attributes.mapValues(convertValue)
26+
}
27+
28+
public func deepCopy() -> ImmutableStructure {
29+
return ImmutableStructure(attributes: attributes)
30+
}
31+
}
32+
33+
extension ImmutableStructure {
34+
private func convertValue(value: Value) -> AnyHashable? {
35+
switch value {
36+
case .boolean(let value):
37+
return value
38+
case .string(let value):
39+
return value
40+
case .integer(let value):
41+
return value
42+
case .double(let value):
43+
return value
44+
case .date(let value):
45+
return value
46+
case .list(let value):
47+
return value.map(convertValue)
48+
case .structure(let value):
49+
return value.mapValues(convertValue)
50+
case .null:
51+
return nil
52+
}
53+
}
54+
}

Sources/OpenFeature/OpenFeatureAPI.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,11 @@ public class OpenFeatureAPI {
156156
self.providerSubject.send(provider)
157157

158158
if let initialContext = initialContext {
159-
self.evaluationContext = initialContext.deepCopy()
159+
self.evaluationContext = initialContext
160160
}
161161

162162
do {
163-
try await provider.initialize(initialContext: initialContext?.deepCopy())
163+
try await provider.initialize(initialContext: initialContext)
164164
self.providerStatus = .ready
165165
self.eventHandler.send(.ready)
166166
} catch {
@@ -177,13 +177,13 @@ public class OpenFeatureAPI {
177177

178178
private func updateContext(evaluationContext: EvaluationContext) async {
179179
do {
180-
let oldContext = self.evaluationContext?.deepCopy()
181-
self.evaluationContext = evaluationContext.deepCopy()
180+
let oldContext = self.evaluationContext
181+
self.evaluationContext = evaluationContext
182182
self.providerStatus = .reconciling
183183
eventHandler.send(.reconciling)
184184
try await self.providerSubject.value?.onContextSet(
185185
oldContext: oldContext,
186-
newContext: evaluationContext.deepCopy()
186+
newContext: evaluationContext
187187
)
188188
self.providerStatus = .ready
189189
eventHandler.send(.contextChanged)

Sources/OpenFeature/MutableContext.swift renamed to Tests/OpenFeatureTests/Helpers/MutableContext.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Foundation
22

3+
@testable import OpenFeature
4+
35
/// The ``MutableContext`` is an ``EvaluationContext`` implementation which is threadsafe, and whose attributes can
46
/// be modified after instantiation.
57
public class MutableContext: EvaluationContext {
@@ -28,12 +30,6 @@ public class MutableContext: EvaluationContext {
2830
}
2931
}
3032

31-
public func setTargetingKey(targetingKey: String) {
32-
queue.sync {
33-
self.targetingKey = targetingKey
34-
}
35-
}
36-
3733
public func keySet() -> Set<String> {
3834
return queue.sync {
3935
structure.keySet()
@@ -57,12 +53,18 @@ public class MutableContext: EvaluationContext {
5753
structure.asObjectMap()
5854
}
5955
}
56+
57+
public func setTargetingKey(targetingKey: String) {
58+
queue.sync {
59+
self.targetingKey = targetingKey
60+
}
61+
}
6062
}
6163

6264
extension MutableContext {
6365
@discardableResult
6466
public func add(key: String, value: Value) -> MutableContext {
65-
queue.sync {
67+
_ = queue.sync {
6668
self.structure.add(key: key, value: value)
6769
}
6870
return self

0 commit comments

Comments
 (0)