diff --git a/Sources/Confidence/ConfidenceClient.swift b/Sources/Confidence/ConfidenceClient.swift index 45f91428..f44ab3ac 100644 --- a/Sources/Confidence/ConfidenceClient.swift +++ b/Sources/Confidence/ConfidenceClient.swift @@ -15,6 +15,7 @@ struct ResolvedValue: Codable, Equatable { var value: ConfidenceValue? var flag: String var resolveReason: ResolveReason + var shouldApply: Bool } public struct ResolvesResult: Codable, Equatable { diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 611aa26a..4e85cef4 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -34,7 +34,7 @@ extension FlagResolution { ) -> Evaluation { do { let parsedKey = try FlagPath.getPath(for: flagName) - let resolvedFlag = self.flags.first { resolvedFlag in resolvedFlag.flag == parsedKey.flag } + let resolvedFlag = self.flags.first { resolvedFlag in resolvedFlag.flag == parsedKey.flag } guard let resolvedFlag = resolvedFlag else { return Evaluation( value: defaultValue, @@ -56,7 +56,9 @@ extension FlagResolution { guard let value = resolvedFlag.value else { // No backend error, but nil value returned. This can happend with "noSegmentMatch" or "archived", for example Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + if (resolvedFlag.shouldApply) { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } } return Evaluation( value: defaultValue, @@ -77,7 +79,9 @@ extension FlagResolution { } if let typedValue = typedValue { Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + if (resolvedFlag.shouldApply) { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } } return Evaluation( value: typedValue, @@ -90,7 +94,9 @@ extension FlagResolution { // `null` type from backend instructs to use client-side default value if parsedValue == .init(null: ()) { Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + if (resolvedFlag.shouldApply) { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } } return Evaluation( value: defaultValue, @@ -111,7 +117,9 @@ extension FlagResolution { } } else { Task { - await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + if (resolvedFlag.shouldApply) { + await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) + } } return Evaluation( value: defaultValue, diff --git a/Sources/Confidence/RemoteResolveConfidenceClient.swift b/Sources/Confidence/RemoteResolveConfidenceClient.swift index 479dead8..a31d3b5c 100644 --- a/Sources/Confidence/RemoteResolveConfidenceClient.swift +++ b/Sources/Confidence/RemoteResolveConfidenceClient.swift @@ -69,7 +69,7 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient { return ResolvedValue( value: nil, flag: try displayName(resolvedFlag: resolvedFlag), - resolveReason: resolvedFlag.reason) + resolveReason: resolvedFlag.reason, shouldApply: true) } let value = ConfidenceValue( @@ -81,7 +81,8 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient { variant: variant, value: value, flag: try displayName(resolvedFlag: resolvedFlag), - resolveReason: resolvedFlag.reason + resolveReason: resolvedFlag.reason, + shouldApply: true ) } diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 655d3143..a375dbaf 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -138,7 +138,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] @@ -174,7 +175,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -216,7 +218,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -253,7 +256,8 @@ class ConfidenceTest: XCTestCase { ResolvedValue( value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .noSegmentMatch) + resolveReason: .noSegmentMatch, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -293,7 +297,8 @@ class ConfidenceTest: XCTestCase { ResolvedValue( value: .init(structure: ["size": .init(null: ())]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -334,7 +339,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -382,7 +388,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -424,7 +431,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(double: 3.14)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -465,7 +473,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -518,7 +527,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -578,7 +588,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(integer: 3)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -628,7 +639,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(boolean: true)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -668,7 +680,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(structure: ["boolean": .init(boolean: true)])]), flag: "flag", - resolveReason: .match + resolveReason: .match, + shouldApply: true ) client.resolvedValues = [value] @@ -710,7 +723,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(null: ())]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -779,7 +793,7 @@ class ConfidenceTest: XCTestCase { let client = FakeClient() client.resolvedValues = - [ResolvedValue(flag: "flag", resolveReason: .targetingKeyError)] + [ResolvedValue(flag: "flag", resolveReason: .targetingKeyError, shouldApply: true)] let confidence = Confidence.Builder(clientSecret: "test") .withContext(initialContext: ["targeting_key": .init(string: "user2")]) @@ -829,7 +843,8 @@ class ConfidenceTest: XCTestCase { variant: "control", value: .init(structure: ["size": .init(boolean: true)]), flag: "flag", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] let confidence = Confidence.Builder(clientSecret: "test") @@ -852,6 +867,40 @@ class ConfidenceTest: XCTestCase { XCTAssertEqual(flagApplier.applyCallCount, 0) } + func testShouldNotApply() async throws { + class FakeClient: ConfidenceResolveClient { + var resolveStats: Int = 0 + var resolvedValues: [ResolvedValue] = [] + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + self.resolveStats += 1 + return .init(resolvedValues: resolvedValues, resolveToken: "token") + } + } + + let client = FakeClient() + client.resolvedValues = [ + ResolvedValue( + variant: "control", + value: .init(structure: ["size": .init(boolean: true)]), + flag: "flag", + resolveReason: .match, + shouldApply: false) + ] + + let confidence = Confidence.Builder(clientSecret: "test") + .withContext(initialContext: ["targeting_key": .init(string: "user2")]) + .withFlagResolverClient(flagResolver: client) + .withFlagApplier(flagApplier: flagApplier) + .build() + + try await confidence.fetchAndActivate() + let _ = confidence.getEvaluation( + key: "flag.size", + defaultValue: false) + + XCTAssertEqual(flagApplier.applyCallCount, 0) + } + func concurrentActivate() async { let confidence = Confidence.Builder(clientSecret: "test") .build() diff --git a/Tests/ConfidenceTests/Helpers/ClientMock.swift b/Tests/ConfidenceTests/Helpers/ClientMock.swift index 659d4c2a..065df3da 100644 --- a/Tests/ConfidenceTests/Helpers/ClientMock.swift +++ b/Tests/ConfidenceTests/Helpers/ClientMock.swift @@ -46,7 +46,7 @@ class ClientMock: ConfidenceResolveClient { func resolve(flag: String, ctx: ConfidenceStruct) throws -> ResolveResult { return ResolveResult( - resolvedValue: ResolvedValue(flag: "flag1", resolveReason: .match), + resolvedValue: ResolvedValue(flag: "flag1", resolveReason: .match, shouldApply: true), resolveToken: "" ) } diff --git a/Tests/ConfidenceTests/LocalStorageResolverTest.swift b/Tests/ConfidenceTests/LocalStorageResolverTest.swift index 1097a266..487a7c1c 100644 --- a/Tests/ConfidenceTests/LocalStorageResolverTest.swift +++ b/Tests/ConfidenceTests/LocalStorageResolverTest.swift @@ -8,7 +8,8 @@ class LocalStorageResolverTest: XCTestCase { let resolvedValue = ResolvedValue( value: .init(structure: ["string": .init(string: "Value")]), flag: "flag_name", - resolveReason: .match + resolveReason: .match, + shouldApply: true ) let flagResolution = FlagResolution( context: ["hey": ConfidenceValue(string: "old value")], @@ -26,7 +27,8 @@ class LocalStorageResolverTest: XCTestCase { let resolvedValue = ResolvedValue( value: .init(structure: ["string": .init(string: "Value")]), flag: "flag_name", - resolveReason: .match + resolveReason: .match, + shouldApply: true ) let context = ["hey": ConfidenceValue(string: "old value")]