From 03db53c1e14ad931deab8a45edf240e725ded7ed Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 5 Aug 2025 16:23:33 +0200 Subject: [PATCH 1/5] feat: Add ImmutableContext implementation Signed-off-by: Fabrizio Demaria --- Sources/OpenFeature/EvaluationContext.swift | 1 - Sources/OpenFeature/ImmutableContext.swift | 51 +++++++++ Sources/OpenFeature/ImmutableStructure.swift | 54 ++++++++++ Sources/OpenFeature/MutableContext.swift | 8 +- Tests/OpenFeatureTests/EvalContextTests.swift | 8 +- .../ImmutableContextTests.swift | 102 ++++++++++++++++++ 6 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 Sources/OpenFeature/ImmutableContext.swift create mode 100644 Sources/OpenFeature/ImmutableStructure.swift create mode 100644 Tests/OpenFeatureTests/ImmutableContextTests.swift diff --git a/Sources/OpenFeature/EvaluationContext.swift b/Sources/OpenFeature/EvaluationContext.swift index 2f72206..97af0bc 100644 --- a/Sources/OpenFeature/EvaluationContext.swift +++ b/Sources/OpenFeature/EvaluationContext.swift @@ -4,5 +4,4 @@ import Foundation public protocol EvaluationContext: Structure { func getTargetingKey() -> String func deepCopy() -> EvaluationContext - func setTargetingKey(targetingKey: String) } diff --git a/Sources/OpenFeature/ImmutableContext.swift b/Sources/OpenFeature/ImmutableContext.swift new file mode 100644 index 0000000..f25e958 --- /dev/null +++ b/Sources/OpenFeature/ImmutableContext.swift @@ -0,0 +1,51 @@ +import Foundation + +/// The ``ImmutableContext`` is an ``EvaluationContext`` implementation which is immutable and thread-safe. +/// It provides read-only access to context data and cannot be modified after creation. +public struct ImmutableContext: EvaluationContext { + private let targetingKey: String + private let structure: ImmutableStructure + + public init(targetingKey: String = "", structure: ImmutableStructure = ImmutableStructure()) { + self.targetingKey = targetingKey + self.structure = structure + } + + public init(attributes: [String: Value]) { + self.init(structure: ImmutableStructure(attributes: attributes)) + } + + public func deepCopy() -> EvaluationContext { + return ImmutableContext(targetingKey: targetingKey, structure: structure.deepCopy()) + } + + public func getTargetingKey() -> String { + return targetingKey + } + + public func keySet() -> Set { + return structure.keySet() + } + + public func getValue(key: String) -> Value? { + return structure.getValue(key: key) + } + + public func asMap() -> [String: Value] { + return structure.asMap() + } + + public func asObjectMap() -> [String: AnyHashable?] { + return structure.asObjectMap() + } +} + +extension ImmutableContext { + /// Creates an immutable context from a mutable context + public init(from mutableContext: MutableContext) { + self.init( + targetingKey: mutableContext.getTargetingKey(), + structure: ImmutableStructure(attributes: mutableContext.asMap()) + ) + } +} diff --git a/Sources/OpenFeature/ImmutableStructure.swift b/Sources/OpenFeature/ImmutableStructure.swift new file mode 100644 index 0000000..eb12e66 --- /dev/null +++ b/Sources/OpenFeature/ImmutableStructure.swift @@ -0,0 +1,54 @@ +import Foundation + +/// The ``ImmutableStructure`` is a ``Structure`` implementation which is immutable and thread-safe. +/// It provides read-only access to structured data and cannot be modified after creation. +public class ImmutableStructure: Structure { + private let attributes: [String: Value] + + public init(attributes: [String: Value] = [:]) { + self.attributes = attributes + } + + public func keySet() -> Set { + return Set(attributes.keys) + } + + public func getValue(key: String) -> Value? { + return attributes[key] + } + + public func asMap() -> [String: Value] { + return attributes + } + + public func asObjectMap() -> [String: AnyHashable?] { + return attributes.mapValues(convertValue) + } + + public func deepCopy() -> ImmutableStructure { + return ImmutableStructure(attributes: attributes) + } +} + +extension ImmutableStructure { + private func convertValue(value: Value) -> AnyHashable? { + switch value { + case .boolean(let value): + return value + case .string(let value): + return value + case .integer(let value): + return value + case .double(let value): + return value + case .date(let value): + return value + case .list(let value): + return value.map(convertValue) + case .structure(let value): + return value.mapValues(convertValue) + case .null: + return nil + } + } +} diff --git a/Sources/OpenFeature/MutableContext.swift b/Sources/OpenFeature/MutableContext.swift index 904e525..9cb7649 100644 --- a/Sources/OpenFeature/MutableContext.swift +++ b/Sources/OpenFeature/MutableContext.swift @@ -28,12 +28,6 @@ public class MutableContext: EvaluationContext { } } - public func setTargetingKey(targetingKey: String) { - queue.sync { - self.targetingKey = targetingKey - } - } - public func keySet() -> Set { return queue.sync { structure.keySet() @@ -62,7 +56,7 @@ public class MutableContext: EvaluationContext { extension MutableContext { @discardableResult public func add(key: String, value: Value) -> MutableContext { - queue.sync { + _ = queue.sync { self.structure.add(key: key, value: value) } return self diff --git a/Tests/OpenFeatureTests/EvalContextTests.swift b/Tests/OpenFeatureTests/EvalContextTests.swift index 42369dd..0501bb2 100644 --- a/Tests/OpenFeatureTests/EvalContextTests.swift +++ b/Tests/OpenFeatureTests/EvalContextTests.swift @@ -4,8 +4,7 @@ import XCTest final class EvalContextTests: XCTestCase { func testContextStoresTargetingKey() { - let ctx = MutableContext() - ctx.setTargetingKey(targetingKey: "test") + let ctx = MutableContext(targetingKey: "test") XCTAssertEqual(ctx.getTargetingKey(), "test") } @@ -166,14 +165,12 @@ final class EvalContextTests: XCTestCase { ) XCTAssertEqual(copiedContext.getValue(key: "structure")?.asStructure()?["nested-int"]?.asInteger(), 200) - originalContext.setTargetingKey(targetingKey: "modified-key") originalContext.add(key: "string", value: .string("modified-value")) originalContext.add(key: "new-key", value: .string("new-value")) XCTAssertEqual(copiedContext.getTargetingKey(), "original-key") XCTAssertEqual(copiedContext.getValue(key: "string")?.asString(), "original-value") XCTAssertNil(copiedContext.getValue(key: "new-key")) - XCTAssertEqual(originalContext.getTargetingKey(), "modified-key") XCTAssertEqual(originalContext.getValue(key: "string")?.asString(), "modified-value") XCTAssertEqual(originalContext.getValue(key: "new-key")?.asString(), "new-value") } @@ -190,7 +187,6 @@ final class EvalContextTests: XCTestCase { XCTAssertTrue(emptyContext.keySet().isEmpty) XCTAssertTrue(copiedContext.keySet().isEmpty) - emptyContext.setTargetingKey(targetingKey: "test") emptyContext.add(key: "key", value: .string("value")) XCTAssertEqual(copiedContext.getTargetingKey(), "") @@ -245,14 +241,12 @@ final class EvalContextTests: XCTestCase { group.enter() concurrentQueue.async { // Modify the context - context.setTargetingKey(targetingKey: "modified-\(i)") context.add(key: "key-\(i)", value: .integer(Int64(i))) // Perform deep copy let copiedContext = context.deepCopy() // Verify the copy is independent - XCTAssertNotEqual(copiedContext.getTargetingKey(), "initial-key") XCTAssertNotNil(copiedContext.getValue(key: "initial")) group.leave() diff --git a/Tests/OpenFeatureTests/ImmutableContextTests.swift b/Tests/OpenFeatureTests/ImmutableContextTests.swift new file mode 100644 index 0000000..bc30fad --- /dev/null +++ b/Tests/OpenFeatureTests/ImmutableContextTests.swift @@ -0,0 +1,102 @@ +import XCTest +@testable import OpenFeature + +final class ImmutableContextTests: XCTestCase { + func testImmutableContextCreation() { + let context = ImmutableContext(targetingKey: "test-key") + + XCTAssertEqual(context.getTargetingKey(), "test-key") + XCTAssertTrue(context.keySet().isEmpty) + } + + func testImmutableContextWithAttributes() { + let attributes: [String: Value] = [ + "string": .string("test-value"), + "integer": .integer(42), + "boolean": .boolean(true), + ] + + let context = ImmutableContext(attributes: attributes) + + XCTAssertEqual(context.getTargetingKey(), "") + XCTAssertEqual(context.keySet().count, 3) + XCTAssertEqual(context.getValue(key: "string")?.asString(), "test-value") + XCTAssertEqual(context.getValue(key: "integer")?.asInteger(), 42) + XCTAssertEqual(context.getValue(key: "boolean")?.asBoolean(), true) + } + + func testImmutableContextDeepCopy() { + let original = ImmutableContext( + targetingKey: "original-key", + structure: ImmutableStructure(attributes: [ + "key": .string("value") + ]) + ) + + guard let copy = original.deepCopy() as? ImmutableContext else { + XCTFail("deepCopy() did not return an ImmutableContext") + return + } + + XCTAssertEqual(copy.getTargetingKey(), "original-key") + XCTAssertEqual(copy.getValue(key: "key")?.asString(), "value") + XCTAssertEqual(original.getTargetingKey(), "original-key") + XCTAssertEqual(original.getValue(key: "key")?.asString(), "value") + } + + func testImmutableContextFromMutableContext() { + let mutableContext = MutableContext(targetingKey: "mutable-key") + mutableContext.add(key: "string", value: .string("mutable-value")) + mutableContext.add(key: "number", value: .integer(123)) + + let immutableContext = ImmutableContext(from: mutableContext) + + XCTAssertEqual(immutableContext.getTargetingKey(), "mutable-key") + XCTAssertEqual(immutableContext.keySet().count, 2) + XCTAssertEqual(immutableContext.getValue(key: "string")?.asString(), "mutable-value") + XCTAssertEqual(immutableContext.getValue(key: "number")?.asInteger(), 123) + } + + func testImmutableContextAsMap() { + let attributes: [String: Value] = [ + "string": .string("test"), + "integer": .integer(42), + "boolean": .boolean(true), + "list": .list([.string("item1"), .integer(100)]), + "structure": .structure([ + "nested": .string("nested-value") + ]), + ] + + let context = ImmutableContext(attributes: attributes) + let map = context.asMap() + + XCTAssertEqual(map.count, 5) + XCTAssertEqual(map["string"]?.asString(), "test") + XCTAssertEqual(map["integer"]?.asInteger(), 42) + XCTAssertEqual(map["boolean"]?.asBoolean(), true) + XCTAssertEqual(map["list"]?.asList()?.count, 2) + XCTAssertEqual(map["structure"]?.asStructure()?["nested"]?.asString(), "nested-value") + } + + func testImmutableContextAsObjectMap() { + let attributes: [String: Value] = [ + "string": .string("test"), + "integer": .integer(42), + "boolean": .boolean(true), + "null": .null, + ] + + let context = ImmutableContext(attributes: attributes) + let objectMap = context.asObjectMap() + + XCTAssertEqual(objectMap.count, 4) + XCTAssertEqual(objectMap["string"] as? String, "test") + XCTAssertEqual(objectMap["integer"] as? Int64, 42) + XCTAssertEqual(objectMap["boolean"] as? Bool, true) + + // For null values, we need to check the unwrapped value + let nullValue = objectMap["null"] + XCTAssertNil(nullValue as? AnyHashable) // But the unwrapped value is nil + } +} From 28c33f3a5dccfa47ed85bcd7d73ff6fdd9fbce25 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 5 Aug 2025 16:55:17 +0200 Subject: [PATCH 2/5] feat: Add safe modifiers to ImmutableContext Signed-off-by: Fabrizio Demaria --- Sources/OpenFeature/ImmutableContext.swift | 24 +++++ .../ImmutableContextTests.swift | 102 ++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/Sources/OpenFeature/ImmutableContext.swift b/Sources/OpenFeature/ImmutableContext.swift index f25e958..5270707 100644 --- a/Sources/OpenFeature/ImmutableContext.swift +++ b/Sources/OpenFeature/ImmutableContext.swift @@ -48,4 +48,28 @@ extension ImmutableContext { structure: ImmutableStructure(attributes: mutableContext.asMap()) ) } + + public func withTargetingKey(_ targetingKey: String) -> ImmutableContext { + return ImmutableContext(targetingKey: targetingKey, structure: structure) + } + + public func setAttribute(key: String, value: Value) -> ImmutableContext { + var newAttributes = structure.asMap() + newAttributes[key] = value + return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) + } + + public func setAttributes(_ attributes: [String: Value]) -> ImmutableContext { + var newAttributes = structure.asMap() + for (key, value) in attributes { + newAttributes[key] = value + } + return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) + } + + public func removeAttribute(key: String) -> ImmutableContext { + var newAttributes = structure.asMap() + newAttributes.removeValue(forKey: key) + return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) + } } diff --git a/Tests/OpenFeatureTests/ImmutableContextTests.swift b/Tests/OpenFeatureTests/ImmutableContextTests.swift index bc30fad..c9c8d2a 100644 --- a/Tests/OpenFeatureTests/ImmutableContextTests.swift +++ b/Tests/OpenFeatureTests/ImmutableContextTests.swift @@ -99,4 +99,106 @@ final class ImmutableContextTests: XCTestCase { let nullValue = objectMap["null"] XCTAssertNil(nullValue as? AnyHashable) // But the unwrapped value is nil } + + func testImmutableContextWithTargetingKey() { + let original = ImmutableContext(targetingKey: "original-key") + let modified = original.withTargetingKey("new-key") + + XCTAssertEqual(original.getTargetingKey(), "original-key") + XCTAssertEqual(modified.getTargetingKey(), "new-key") + XCTAssertTrue(original.keySet().isEmpty) + XCTAssertTrue(modified.keySet().isEmpty) + } + + func testImmutableContextSetAttribute() { + let original = ImmutableContext(targetingKey: "user-123") + let modified = original.setAttribute(key: "country", value: .string("US")) + + XCTAssertEqual(original.getTargetingKey(), "user-123") + XCTAssertEqual(modified.getTargetingKey(), "user-123") + XCTAssertTrue(original.keySet().isEmpty) + XCTAssertEqual(modified.keySet().count, 1) + XCTAssertEqual(modified.getValue(key: "country")?.asString(), "US") + XCTAssertNil(original.getValue(key: "country")) + } + + func testImmutableContextSetMultipleAttributes() { + let original = ImmutableContext(targetingKey: "user-123") + let attributes: [String: Value] = [ + "country": .string("US"), + "age": .integer(25), + "premium": .boolean(true), + ] + let modified = original.setAttributes(attributes) + + XCTAssertEqual(original.getTargetingKey(), "user-123") + XCTAssertEqual(modified.getTargetingKey(), "user-123") + XCTAssertTrue(original.keySet().isEmpty) + XCTAssertEqual(modified.keySet().count, 3) + XCTAssertEqual(modified.getValue(key: "country")?.asString(), "US") + XCTAssertEqual(modified.getValue(key: "age")?.asInteger(), 25) + XCTAssertEqual(modified.getValue(key: "premium")?.asBoolean(), true) + } + + func testImmutableContextRemoveAttribute() { + let original = ImmutableContext( + targetingKey: "user-123", + structure: ImmutableStructure(attributes: [ + "country": .string("US"), + "age": .integer(25), + "premium": .boolean(true), + ]) + ) + let modified = original.removeAttribute(key: "age") + + XCTAssertEqual(original.getTargetingKey(), "user-123") + XCTAssertEqual(modified.getTargetingKey(), "user-123") + XCTAssertEqual(original.keySet().count, 3) + XCTAssertEqual(modified.keySet().count, 2) + XCTAssertEqual(original.getValue(key: "age")?.asInteger(), 25) + XCTAssertNil(modified.getValue(key: "age")) + XCTAssertEqual(modified.getValue(key: "country")?.asString(), "US") + XCTAssertEqual(modified.getValue(key: "premium")?.asBoolean(), true) + } + + func testImmutableContextChaining() { + let context = ImmutableContext(targetingKey: "user-123") + .setAttribute(key: "country", value: .string("US")) + .setAttribute(key: "age", value: .integer(25)) + .setAttribute(key: "premium", value: .boolean(true)) + + XCTAssertEqual(context.getTargetingKey(), "user-123") + XCTAssertEqual(context.keySet().count, 3) + XCTAssertEqual(context.getValue(key: "country")?.asString(), "US") + XCTAssertEqual(context.getValue(key: "age")?.asInteger(), 25) + XCTAssertEqual(context.getValue(key: "premium")?.asBoolean(), true) + } + + func testImmutableContextThreadSafetyWithModifications() { + let original = ImmutableContext(targetingKey: "user-123") + .setAttribute(key: "country", value: .string("US")) + + let expectation = XCTestExpectation(description: "Thread safety with modifications test") + expectation.expectedFulfillmentCount = 10 + + DispatchQueue.concurrentPerform(iterations: 10) { index in + let modified = original + .setAttribute(key: "thread", value: .integer(Int64(index))) + .setAttribute(key: "timestamp", value: .double(Double(index))) + + XCTAssertEqual(modified.getTargetingKey(), "user-123") + XCTAssertEqual(modified.getValue(key: "country")?.asString(), "US") + XCTAssertEqual(modified.getValue(key: "thread")?.asInteger(), Int64(index)) + XCTAssertEqual(modified.getValue(key: "timestamp")?.asDouble(), Double(index)) + + XCTAssertEqual(original.keySet().count, 1) + XCTAssertEqual(original.getValue(key: "country")?.asString(), "US") + XCTAssertNil(original.getValue(key: "thread")) + XCTAssertNil(original.getValue(key: "timestamp")) + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1.0) + } } From fae07bdd023ad4ad0a09c09690d1bee503851c66 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 5 Aug 2025 16:57:35 +0200 Subject: [PATCH 3/5] refactor!: Dont expose MutableContext Signed-off-by: Fabrizio Demaria --- Sources/OpenFeature/ImmutableContext.swift | 8 -------- Tests/OpenFeatureTests/EvalContextTests.swift | 8 +++++++- .../OpenFeatureTests/Helpers}/MutableContext.swift | 8 ++++++++ Tests/OpenFeatureTests/ImmutableContextTests.swift | 9 +++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) rename {Sources/OpenFeature => Tests/OpenFeatureTests/Helpers}/MutableContext.swift (91%) diff --git a/Sources/OpenFeature/ImmutableContext.swift b/Sources/OpenFeature/ImmutableContext.swift index 5270707..f24e215 100644 --- a/Sources/OpenFeature/ImmutableContext.swift +++ b/Sources/OpenFeature/ImmutableContext.swift @@ -41,14 +41,6 @@ public struct ImmutableContext: EvaluationContext { } extension ImmutableContext { - /// Creates an immutable context from a mutable context - public init(from mutableContext: MutableContext) { - self.init( - targetingKey: mutableContext.getTargetingKey(), - structure: ImmutableStructure(attributes: mutableContext.asMap()) - ) - } - public func withTargetingKey(_ targetingKey: String) -> ImmutableContext { return ImmutableContext(targetingKey: targetingKey, structure: structure) } diff --git a/Tests/OpenFeatureTests/EvalContextTests.swift b/Tests/OpenFeatureTests/EvalContextTests.swift index 0501bb2..42369dd 100644 --- a/Tests/OpenFeatureTests/EvalContextTests.swift +++ b/Tests/OpenFeatureTests/EvalContextTests.swift @@ -4,7 +4,8 @@ import XCTest final class EvalContextTests: XCTestCase { func testContextStoresTargetingKey() { - let ctx = MutableContext(targetingKey: "test") + let ctx = MutableContext() + ctx.setTargetingKey(targetingKey: "test") XCTAssertEqual(ctx.getTargetingKey(), "test") } @@ -165,12 +166,14 @@ final class EvalContextTests: XCTestCase { ) XCTAssertEqual(copiedContext.getValue(key: "structure")?.asStructure()?["nested-int"]?.asInteger(), 200) + originalContext.setTargetingKey(targetingKey: "modified-key") originalContext.add(key: "string", value: .string("modified-value")) originalContext.add(key: "new-key", value: .string("new-value")) XCTAssertEqual(copiedContext.getTargetingKey(), "original-key") XCTAssertEqual(copiedContext.getValue(key: "string")?.asString(), "original-value") XCTAssertNil(copiedContext.getValue(key: "new-key")) + XCTAssertEqual(originalContext.getTargetingKey(), "modified-key") XCTAssertEqual(originalContext.getValue(key: "string")?.asString(), "modified-value") XCTAssertEqual(originalContext.getValue(key: "new-key")?.asString(), "new-value") } @@ -187,6 +190,7 @@ final class EvalContextTests: XCTestCase { XCTAssertTrue(emptyContext.keySet().isEmpty) XCTAssertTrue(copiedContext.keySet().isEmpty) + emptyContext.setTargetingKey(targetingKey: "test") emptyContext.add(key: "key", value: .string("value")) XCTAssertEqual(copiedContext.getTargetingKey(), "") @@ -241,12 +245,14 @@ final class EvalContextTests: XCTestCase { group.enter() concurrentQueue.async { // Modify the context + context.setTargetingKey(targetingKey: "modified-\(i)") context.add(key: "key-\(i)", value: .integer(Int64(i))) // Perform deep copy let copiedContext = context.deepCopy() // Verify the copy is independent + XCTAssertNotEqual(copiedContext.getTargetingKey(), "initial-key") XCTAssertNotNil(copiedContext.getValue(key: "initial")) group.leave() diff --git a/Sources/OpenFeature/MutableContext.swift b/Tests/OpenFeatureTests/Helpers/MutableContext.swift similarity index 91% rename from Sources/OpenFeature/MutableContext.swift rename to Tests/OpenFeatureTests/Helpers/MutableContext.swift index 9cb7649..b2168e4 100644 --- a/Sources/OpenFeature/MutableContext.swift +++ b/Tests/OpenFeatureTests/Helpers/MutableContext.swift @@ -1,5 +1,7 @@ import Foundation +@testable import OpenFeature + /// The ``MutableContext`` is an ``EvaluationContext`` implementation which is threadsafe, and whose attributes can /// be modified after instantiation. public class MutableContext: EvaluationContext { @@ -51,6 +53,12 @@ public class MutableContext: EvaluationContext { structure.asObjectMap() } } + + public func setTargetingKey(targetingKey: String) { + queue.sync { + self.targetingKey = targetingKey + } + } } extension MutableContext { diff --git a/Tests/OpenFeatureTests/ImmutableContextTests.swift b/Tests/OpenFeatureTests/ImmutableContextTests.swift index c9c8d2a..1c5d94d 100644 --- a/Tests/OpenFeatureTests/ImmutableContextTests.swift +++ b/Tests/OpenFeatureTests/ImmutableContextTests.swift @@ -202,3 +202,12 @@ final class ImmutableContextTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } } + +extension ImmutableContext { + public init(from mutableContext: MutableContext) { + self.init( + targetingKey: mutableContext.getTargetingKey(), + structure: ImmutableStructure(attributes: mutableContext.asMap()) + ) + } +} From 93be746566b44ba76a23a7858d724e7e1dd19d47 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 6 Aug 2025 10:09:12 +0200 Subject: [PATCH 4/5] chore: Remove unnecessary copies Signed-off-by: Fabrizio Demaria --- Sources/OpenFeature/OpenFeatureAPI.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index fa71dca..27b8dea 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -156,11 +156,11 @@ public class OpenFeatureAPI { self.providerSubject.send(provider) if let initialContext = initialContext { - self.evaluationContext = initialContext.deepCopy() + self.evaluationContext = initialContext } do { - try await provider.initialize(initialContext: initialContext?.deepCopy()) + try await provider.initialize(initialContext: initialContext) self.providerStatus = .ready self.eventHandler.send(.ready) } catch { @@ -177,13 +177,13 @@ public class OpenFeatureAPI { private func updateContext(evaluationContext: EvaluationContext) async { do { - let oldContext = self.evaluationContext?.deepCopy() - self.evaluationContext = evaluationContext.deepCopy() + let oldContext = self.evaluationContext + self.evaluationContext = evaluationContext self.providerStatus = .reconciling eventHandler.send(.reconciling) try await self.providerSubject.value?.onContextSet( oldContext: oldContext, - newContext: evaluationContext.deepCopy() + newContext: evaluationContext ) self.providerStatus = .ready eventHandler.send(.contextChanged) From 97c748cab882c29c8f833fe846953144413deea5 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Wed, 6 Aug 2025 10:29:12 +0200 Subject: [PATCH 5/5] refactor: Rename context functions Signed-off-by: Fabrizio Demaria --- Sources/OpenFeature/ImmutableContext.swift | 6 +++--- .../ImmutableContextTests.swift | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/OpenFeature/ImmutableContext.swift b/Sources/OpenFeature/ImmutableContext.swift index f24e215..252cc16 100644 --- a/Sources/OpenFeature/ImmutableContext.swift +++ b/Sources/OpenFeature/ImmutableContext.swift @@ -45,13 +45,13 @@ extension ImmutableContext { return ImmutableContext(targetingKey: targetingKey, structure: structure) } - public func setAttribute(key: String, value: Value) -> ImmutableContext { + public func withAttribute(key: String, value: Value) -> ImmutableContext { var newAttributes = structure.asMap() newAttributes[key] = value return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) } - public func setAttributes(_ attributes: [String: Value]) -> ImmutableContext { + public func withAttributes(_ attributes: [String: Value]) -> ImmutableContext { var newAttributes = structure.asMap() for (key, value) in attributes { newAttributes[key] = value @@ -59,7 +59,7 @@ extension ImmutableContext { return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) } - public func removeAttribute(key: String) -> ImmutableContext { + public func withoutAttribute(key: String) -> ImmutableContext { var newAttributes = structure.asMap() newAttributes.removeValue(forKey: key) return ImmutableContext(targetingKey: targetingKey, structure: ImmutableStructure(attributes: newAttributes)) diff --git a/Tests/OpenFeatureTests/ImmutableContextTests.swift b/Tests/OpenFeatureTests/ImmutableContextTests.swift index 1c5d94d..2557a31 100644 --- a/Tests/OpenFeatureTests/ImmutableContextTests.swift +++ b/Tests/OpenFeatureTests/ImmutableContextTests.swift @@ -112,7 +112,7 @@ final class ImmutableContextTests: XCTestCase { func testImmutableContextSetAttribute() { let original = ImmutableContext(targetingKey: "user-123") - let modified = original.setAttribute(key: "country", value: .string("US")) + let modified = original.withAttribute(key: "country", value: .string("US")) XCTAssertEqual(original.getTargetingKey(), "user-123") XCTAssertEqual(modified.getTargetingKey(), "user-123") @@ -129,7 +129,7 @@ final class ImmutableContextTests: XCTestCase { "age": .integer(25), "premium": .boolean(true), ] - let modified = original.setAttributes(attributes) + let modified = original.withAttributes(attributes) XCTAssertEqual(original.getTargetingKey(), "user-123") XCTAssertEqual(modified.getTargetingKey(), "user-123") @@ -149,7 +149,7 @@ final class ImmutableContextTests: XCTestCase { "premium": .boolean(true), ]) ) - let modified = original.removeAttribute(key: "age") + let modified = original.withoutAttribute(key: "age") XCTAssertEqual(original.getTargetingKey(), "user-123") XCTAssertEqual(modified.getTargetingKey(), "user-123") @@ -163,9 +163,9 @@ final class ImmutableContextTests: XCTestCase { func testImmutableContextChaining() { let context = ImmutableContext(targetingKey: "user-123") - .setAttribute(key: "country", value: .string("US")) - .setAttribute(key: "age", value: .integer(25)) - .setAttribute(key: "premium", value: .boolean(true)) + .withAttribute(key: "country", value: .string("US")) + .withAttribute(key: "age", value: .integer(25)) + .withAttribute(key: "premium", value: .boolean(true)) XCTAssertEqual(context.getTargetingKey(), "user-123") XCTAssertEqual(context.keySet().count, 3) @@ -176,15 +176,15 @@ final class ImmutableContextTests: XCTestCase { func testImmutableContextThreadSafetyWithModifications() { let original = ImmutableContext(targetingKey: "user-123") - .setAttribute(key: "country", value: .string("US")) + .withAttribute(key: "country", value: .string("US")) let expectation = XCTestExpectation(description: "Thread safety with modifications test") expectation.expectedFulfillmentCount = 10 DispatchQueue.concurrentPerform(iterations: 10) { index in let modified = original - .setAttribute(key: "thread", value: .integer(Int64(index))) - .setAttribute(key: "timestamp", value: .double(Double(index))) + .withAttribute(key: "thread", value: .integer(Int64(index))) + .withAttribute(key: "timestamp", value: .double(Double(index))) XCTAssertEqual(modified.getTargetingKey(), "user-123") XCTAssertEqual(modified.getValue(key: "country")?.asString(), "US")