From d0dddee43da840bb4d31c645295a2cb002aefcfc Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 9 Apr 2024 16:45:24 +0200 Subject: [PATCH] feat: Implement `withContext` (#89) * Implement withContext * Add ConfidenceContextProvider protocol * Remove context on child instance * Remove clear from Contextual --- Sources/Confidence/Confidence.swift | 39 +++- .../ConfidenceContextProvider.swift | 6 + Sources/Confidence/Contextual.swift | 14 +- .../ConfidenceFeatureProviderTest.swift | 8 +- Tests/ConfidenceTests/ConfidenceTests.swift | 183 ++++++++++++++++++ 5 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 Sources/Confidence/ConfidenceContextProvider.swift create mode 100644 Tests/ConfidenceTests/ConfidenceTests.swift diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 09d8208c..e0006979 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -1,23 +1,28 @@ import Foundation public class Confidence: ConfidenceEventSender { - public var context: ConfidenceStruct + private let parent: ConfidenceContextProvider? + private var context: ConfidenceStruct public let clientSecret: String public var timeout: TimeInterval public var region: ConfidenceRegion public var initializationStrategy: InitializationStrategy + private var removedContextKeys: Set = Set() - init( + required public init( clientSecret: String, timeout: TimeInterval, region: ConfidenceRegion, - initializationStrategy: InitializationStrategy + initializationStrategy: InitializationStrategy, + context: ConfidenceStruct = [:], + parent: ConfidenceEventSender? = nil ) { - self.context = [:] self.clientSecret = clientSecret self.timeout = timeout self.region = region self.initializationStrategy = initializationStrategy + self.context = context + self.parent = parent } // TODO: Implement actual event uploading to the backend @@ -25,21 +30,35 @@ public class Confidence: ConfidenceEventSender { print("Sending: \"\(definition)\".\nMessage: \(payload)\nContext: \(context)") } + + public func getContext() -> ConfidenceStruct { + let parentContext = parent?.getContext() ?? [:] + var reconciledCtx = parentContext.filter { + !removedContextKeys.contains($0.key) + } + self.context.forEach { entry in + reconciledCtx.updateValue(entry.value, forKey: entry.key) + } + return reconciledCtx + } + public func updateContextEntry(key: String, value: ConfidenceValue) { context[key] = value } public func removeContextEntry(key: String) { context.removeValue(forKey: key) + removedContextKeys.insert(key) } - public func clearContext() { - context = [:] - } - - // TODO: Implement creation of child instances public func withContext(_ context: ConfidenceStruct) -> Self { - return self + return Self.init( + clientSecret: clientSecret, + timeout: timeout, + region: region, + initializationStrategy: initializationStrategy, + context: context, + parent: self) } } diff --git a/Sources/Confidence/ConfidenceContextProvider.swift b/Sources/Confidence/ConfidenceContextProvider.swift new file mode 100644 index 00000000..b9e5424a --- /dev/null +++ b/Sources/Confidence/ConfidenceContextProvider.swift @@ -0,0 +1,6 @@ +import Foundation + +/// A Contextual implementer returns the current context +public protocol ConfidenceContextProvider { + func getContext() -> ConfidenceStruct +} diff --git a/Sources/Confidence/Contextual.swift b/Sources/Confidence/Contextual.swift index 390279a6..554babd5 100644 --- a/Sources/Confidence/Contextual.swift +++ b/Sources/Confidence/Contextual.swift @@ -1,14 +1,14 @@ import Foundation -/// A Contextual implementer maintains context data and can create child instances +/// A Contextual implementer maintains local context data and can create child instances /// that can still access their parent's data -public protocol Contextual { - var context: ConfidenceStruct { get set } - +/// Each ConfidenceContextProvider returns local data reconciled with parents' data. Local data has precedence +public protocol Contextual: ConfidenceContextProvider { + /// Adds/override entry to local data func updateContextEntry(key: String, value: ConfidenceValue) + /// Removes entry from local data + /// It hides entries with this key from parents' data (without modifying parents' data) func removeContextEntry(key: String) - func clearContext() - /// Creates a child Contextual instance that still has access - /// to its parent context + /// Creates a child Contextual instance that maintains access to its parent's data func withContext(_ context: ConfidenceStruct) -> Self } diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 919f71f8..759d2bda 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -913,7 +913,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { { provider.initialize(initialContext: MutableContext(targetingKey: "user1")) wait(for: [readyExpectation], timeout: 5) - let context = confidence.context + let context = confidence.getContext() let expected = [ "open_feature": ConfidenceValue(structure: ["targeting_key": ConfidenceValue(string: "user1")]) ] @@ -936,11 +936,13 @@ class ConfidenceFeatureProviderTest: XCTestCase { }) { let ctx1 = MutableContext(targetingKey: "user1") - let ctx2 = MutableContext(targetingKey: "user1", structure: MutableStructure(attributes: ["active": Value.boolean(true)])) + let ctx2 = MutableContext( + targetingKey: "user1", + structure: MutableStructure(attributes: ["active": Value.boolean(true)])) provider.initialize(initialContext: ctx1) provider.onContextSet(oldContext: ctx1, newContext: ctx2) wait(for: [readyExpectation], timeout: 5) - let context = confidence.context + let context = confidence.getContext() let expected = [ "open_feature": ConfidenceValue(structure: [ "targeting_key": ConfidenceValue(string: "user1"), diff --git a/Tests/ConfidenceTests/ConfidenceTests.swift b/Tests/ConfidenceTests/ConfidenceTests.swift new file mode 100644 index 00000000..b77e05a5 --- /dev/null +++ b/Tests/ConfidenceTests/ConfidenceTests.swift @@ -0,0 +1,183 @@ +import Confidence +import XCTest + +final class ConfidenceTests: XCTestCase { + func testWithContext() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + ["k2": ConfidenceValue(string: "v2")] + ) + let expected = [ + "k1": ConfidenceValue(string: "v1"), + "k2": ConfidenceValue(string: "v2") + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testWithContextUpdateParent() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + ["k2": ConfidenceValue(string: "v2")] + ) + confidenceParent.updateContextEntry( + key: "k3", + value: ConfidenceValue(string: "v3")) + let expected = [ + "k1": ConfidenceValue(string: "v1"), + "k2": ConfidenceValue(string: "v2"), + "k3": ConfidenceValue(string: "v3"), + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testUpdateLocalContext() { + let confidence = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + confidence.updateContextEntry( + key: "k1", + value: ConfidenceValue(string: "v3")) + let expected = [ + "k1": ConfidenceValue(string: "v3"), + ] + XCTAssertEqual(confidence.getContext(), expected) + } + + func testUpdateLocalContextWithoutOverride() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + ["k2": ConfidenceValue(string: "v2")] + ) + confidenceChild.updateContextEntry( + key: "k2", + value: ConfidenceValue(string: "v4")) + let expected = [ + "k1": ConfidenceValue(string: "v1"), + "k2": ConfidenceValue(string: "v4"), + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testUpdateParentContextWithOverride() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + ["k2": ConfidenceValue(string: "v2")] + ) + confidenceParent.updateContextEntry( + key: "k2", + value: ConfidenceValue(string: "v4")) + let expected = [ + "k1": ConfidenceValue(string: "v1"), + "k2": ConfidenceValue(string: "v2"), + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testRemoveContextEntry() { + let confidence = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: [ + "k1": ConfidenceValue(string: "v1"), + "k2": ConfidenceValue(string: "v2") + ] + ) + confidence.removeContextEntry(key: "k2") + let expected = [ + "k1": ConfidenceValue(string: "v1") + ] + XCTAssertEqual(confidence.getContext(), expected) + } + + func testRemoveContextEntryFromParent() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + ["k2": ConfidenceValue(string: "v2")] + ) + confidenceChild.removeContextEntry(key: "k1") + let expected = [ + "k2": ConfidenceValue(string: "v2") + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testRemoveContextEntryFromParentAndChild() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + [ + "k2": ConfidenceValue(string: "v2"), + "k1": ConfidenceValue(string: "v3"), + ] + ) + confidenceChild.removeContextEntry(key: "k1") + let expected = [ + "k2": ConfidenceValue(string: "v2") + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } + + func testRemoveContextEntryFromParentAndChildThenUpdate() { + let confidenceParent = Confidence.init( + clientSecret: "", + timeout: TimeInterval(), + region: .europe, + initializationStrategy: .activateAndFetchAsync, + context: ["k1": ConfidenceValue(string: "v1")] + ) + let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( + [ + "k2": ConfidenceValue(string: "v2"), + "k1": ConfidenceValue(string: "v3"), + ] + ) + confidenceChild.removeContextEntry(key: "k1") + confidenceChild.updateContextEntry(key: "k1", value: ConfidenceValue(string: "v4")) + let expected = [ + "k2": ConfidenceValue(string: "v2"), + "k1": ConfidenceValue(string: "v4"), + ] + XCTAssertEqual(confidenceChild.getContext(), expected) + } +}