From a4b1e0af5ccfc83d61ca566a78f4a4181b112d63 Mon Sep 17 00:00:00 2001 From: Nicky <31034418+nickybondarenko@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:01:24 +0100 Subject: [PATCH] feat: add shouldApply to control sending apply (#188) --- Sources/Confidence/ConfidenceClient.swift | 1 + Sources/Confidence/FlagEvaluation.swift | 20 +++-- .../RemoteResolveConfidenceClient.swift | 7 +- .../ConfidenceProviderTest.swift | 3 +- Tests/ConfidenceTests/ConfidenceTest.swift | 81 +++++++++++++++---- .../ConfidenceTests/Helpers/ClientMock.swift | 2 +- .../LocalStorageResolverTest.swift | 6 +- 7 files changed, 93 insertions(+), 27 deletions(-) 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..393dd8e1 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -25,6 +25,7 @@ struct FlagResolution: Encodable, Decodable, Equatable { extension FlagResolution { // swiftlint:disable function_body_length + // swiftlint:disable cyclomatic_complexity func evaluate( flagName: String, defaultValue: T, @@ -34,7 +35,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 +57,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 +80,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 +95,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 +118,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, @@ -132,6 +141,7 @@ extension FlagResolution { } } // swiftlint:enable function_body_length + // swiftlint:enable cyclomatic_complexity private func checkBackendErrors(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation? { if resolvedFlag.resolveReason == .targetingKeyError { diff --git a/Sources/Confidence/RemoteResolveConfidenceClient.swift b/Sources/Confidence/RemoteResolveConfidenceClient.swift index 479dead8..e8ac167a 100644 --- a/Sources/Confidence/RemoteResolveConfidenceClient.swift +++ b/Sources/Confidence/RemoteResolveConfidenceClient.swift @@ -69,7 +69,9 @@ 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 +83,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/ConfidenceProviderTests/ConfidenceProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift index 0f34e03e..4d37f700 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift @@ -88,7 +88,8 @@ class ConfidenceProviderTest: XCTestCase { variant: "variant1", value: .init(structure: ["int": .init(integer: 42)]), flag: "flagName", - resolveReason: .match) + resolveReason: .match, + shouldApply: true) ] func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { return .init(resolvedValues: resolvedValues, resolveToken: "token") diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index 655d3143..235cd9b2 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() + _ = 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")]