From 8256cf8f67744a1a2c444eb84996af075f22a96e Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Fri, 7 Jun 2024 16:51:16 +0200 Subject: [PATCH] feat: Differentiate OF non-OF in metadata --- Sources/Confidence/Confidence.swift | 90 +++++++++++++------ Sources/Confidence/ConfidenceClient.swift | 2 +- Sources/Confidence/FlagEvaluation.swift | 3 +- .../RemoteResolveConfidenceClient.swift | 8 +- .../ConfidenceFeatureProvider.swift | 19 ++-- .../ConfidenceFeatureProviderTest.swift | 24 ++--- .../ConfidenceTests/Helpers/ClientMock.swift | 2 +- .../LocalStorageResolverTest.swift | 8 +- .../RemoteResolveConfidenceClientTest.swift | 2 +- 9 files changed, 102 insertions(+), 56 deletions(-) diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 2db8b68c..b34d2f91 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -7,7 +7,8 @@ public class Confidence: ConfidenceEventSender { public var region: ConfidenceRegion private let parent: ConfidenceContextProvider? private let eventSenderEngine: EventSenderEngine - private let contextSubject = CurrentValueSubject([:]) + private let contextSubject = CurrentValueSubject( + ContextUpdateSignal.init(context: [:], isProvider: false)) private var removedContextKeys: Set = Set() private let confidenceQueue = DispatchQueue(label: "com.confidence.queue") private let remoteFlagResolver: ConfidenceResolveClient @@ -33,7 +34,7 @@ public class Confidence: ConfidenceEventSender { self.clientSecret = clientSecret self.region = region self.storage = storage - self.contextSubject.value = context + self.contextSubject.value = ContextUpdateSignal.init(context: context, isProvider: false) self.parent = parent self.storage = storage self.flagApplier = flagApplier @@ -42,7 +43,7 @@ public class Confidence: ConfidenceEventSender { putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) } - contextChanges().sink { [weak self] context in + contextChanges().sink { [weak self] signal in guard let self = self else { return } @@ -50,7 +51,7 @@ public class Confidence: ConfidenceEventSender { self.currentFetchTask = Task { do { let context = self.getContext() - try await self.fetchAndActivate() + try await self.fetchAndActivate(isProvider: signal.isProvider) self.contextReconciliatedChanges.send(context.hash()) } catch { } @@ -64,14 +65,20 @@ public class Confidence: ConfidenceEventSender { self.cache = savedFlags } - public func fetchAndActivate() async throws { - try await internalFetch() + /** + Fetches flag evaluations from backend and returns once the fetch is + complete (regardless of success) + Args: + - isProvider: used in case of OpenFeature integration, do not override manually + */ + public func fetchAndActivate(isProvider: Bool = false) async throws { + try await internalFetch(isProvider: isProvider) try activate() } - func internalFetch() async throws { + func internalFetch(isProvider: Bool) async throws { let context = getContext() - let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context) + let resolvedFlags = try await remoteFlagResolver.resolve(ctx: context, isProvider: isProvider) let resolution = FlagResolution( context: context, flags: resolvedFlags.resolvedValues, @@ -80,24 +87,39 @@ public class Confidence: ConfidenceEventSender { try storage.save(data: resolution) } - public func asyncFetch() { + /** + Start a network request to fetch flag evaluations from backend, returns immediately. + In case of success storage cache will be updated for future sessions, but in-session cache remains unchanged. + Args: + - isProvider: used in case of OpenFeature integration, do not override manually + */ + public func asyncFetch(isProvider: Bool = false) { Task { - try await internalFetch() + try await internalFetch(isProvider: isProvider) } } - public func getEvaluation(key: String, defaultValue: T) throws -> Evaluation { + /** + Read flag evaluation from local cache. Does not trigger a backend request. + Args: + - key: flag name followed by the path of the property you want to access (dot notation expected). + e.g. "my-flag.my-property" + - defaultValue: value return in case of errors, for example if the flag/property is not found + - isProvider: used in case of OpenFeature integration, do not override manually + */ + public func getEvaluation(key: String, defaultValue: T, isProvider: Bool = false) throws -> Evaluation { try self.cache.evaluate( flagName: key, defaultValue: defaultValue, context: getContext(), - flagApplier: flagApplier + flagApplier: flagApplier, + isProvider: isProvider ) } public func getValue(key: String, defaultValue: T) -> T { do { - return try getEvaluation(key: key, defaultValue: defaultValue).value + return try getEvaluation(key: key, defaultValue: defaultValue, isProvider: false).value } catch { return defaultValue } @@ -107,7 +129,7 @@ public class Confidence: ConfidenceEventSender { return storage.isEmpty() } - public func contextChanges() -> AnyPublisher { + private func contextChanges() -> AnyPublisher { return contextSubject .dropFirst() .removeDuplicates() @@ -172,7 +194,7 @@ public class Confidence: ConfidenceEventSender { var reconciledCtx = parentContext.filter { !removedContextKeys.contains($0.key) } - self.contextSubject.value.forEach { entry in + self.contextSubject.value.context.forEach { entry in reconciledCtx.updateValue(entry.value, forKey: entry.key) } return reconciledCtx @@ -180,40 +202,47 @@ public class Confidence: ConfidenceEventSender { public func putContext(key: String, value: ConfidenceValue) { withLock { confidence in - var map = confidence.contextSubject.value + var map = confidence.contextSubject.value.context map[key] = value - confidence.contextSubject.value = map + confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false) } } public func putContext(context: ConfidenceStruct) { withLock { confidence in - var map = confidence.contextSubject.value + var map = confidence.contextSubject.value.context for entry in context { map.updateValue(entry.value, forKey: entry.key) } - confidence.contextSubject.value = map + confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false) } } - public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = []) { + /** + Updates the context. This will trigger a new fetchAndActivate thus updating the cache mid-session. + Args: + - context: entries to be appended to the context + - removeKeys: keys of the entries that are removed from the context (including parent entries) + - isProvider: used in case of OpenFeature integration, do not override manually + */ + public func putContext(context: ConfidenceStruct, removeKeys removedKeys: [String] = [], isProvider: Bool = false) { withLock { confidence in - var map = confidence.contextSubject.value + var map = confidence.contextSubject.value.context for removedKey in removedKeys { map.removeValue(forKey: removedKey) } for entry in context { map.updateValue(entry.value, forKey: entry.key) } - confidence.contextSubject.value = map + confidence.contextSubject.value = ContextUpdateSignal.init(context: map, isProvider: isProvider) } } public func removeKey(key: String) { withLock { confidence in - var map = confidence.contextSubject.value + var map = confidence.contextSubject.value.context map.removeValue(forKey: key) - confidence.contextSubject.value = map + confidence.contextSubject.value = ContextUpdateSignal(context: map, isProvider: false) confidence.removedContextKeys.insert(key) } } @@ -243,7 +272,7 @@ extension Confidence { var visitorId = VisitorUtil().getId() var initialContext: ConfidenceStruct = [:] - /** + /*** Initializes the builder with the given credentails. */ public init(clientSecret: String) { @@ -276,7 +305,7 @@ extension Confidence { return self } - /** + /*** Sets the region for the network request to the Confidence backend. The default is `global` and the requests are automatically routed to the closest server. */ @@ -325,4 +354,13 @@ extension Confidence { ) } } + + private struct ContextUpdateSignal: Equatable, Hashable { + let context: ConfidenceStruct + let isProvider: Bool + + func hash(into hasher: inout Hasher) { + hasher.combine(context.hash()) + } + } } diff --git a/Sources/Confidence/ConfidenceClient.swift b/Sources/Confidence/ConfidenceClient.swift index 45f91428..8e2366b1 100644 --- a/Sources/Confidence/ConfidenceClient.swift +++ b/Sources/Confidence/ConfidenceClient.swift @@ -7,7 +7,7 @@ protocol ConfidenceClient { protocol ConfidenceResolveClient { // Async - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult } struct ResolvedValue: Codable, Equatable { diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index c69afa93..877a47ae 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -25,7 +25,8 @@ extension FlagResolution { flagName: String, defaultValue: T, context: ConfidenceStruct, - flagApplier: FlagApplier? = nil + flagApplier: FlagApplier? = nil, + isProvider: Bool ) throws -> Evaluation { let parsedKey = try FlagPath.getPath(for: flagName) if self == FlagResolution.EMPTY { diff --git a/Sources/Confidence/RemoteResolveConfidenceClient.swift b/Sources/Confidence/RemoteResolveConfidenceClient.swift index 7521f75a..8cb45584 100644 --- a/Sources/Confidence/RemoteResolveConfidenceClient.swift +++ b/Sources/Confidence/RemoteResolveConfidenceClient.swift @@ -24,13 +24,13 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { // MARK: Resolver - public func resolve(flags: [String], ctx: ConfidenceStruct) async throws -> ResolvesResult { + public func resolve(flags: [String], ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { let request = ResolveFlagsRequest( flags: flags.map { "flags/\($0)" }, evaluationContext: TypeMapper.convert(structure: ctx), clientSecret: options.credentials.getSecret(), apply: applyOnResolve, - sdk: Sdk(id: metadata.name, version: metadata.version) + sdk: Sdk(id: isProvider ? "SDK_ID_SWIFT_PROVIDER" : metadata.name, version: metadata.version) ) do { @@ -54,8 +54,8 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { } } - public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { - return try await resolve(flags: [], ctx: ctx) + public func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { + return try await resolve(flags: [], ctx: ctx, isProvider: isProvider) } // MARK: Private diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 5319156a..bdeda027 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -52,10 +52,10 @@ public class ConfidenceFeatureProvider: FeatureProvider { if initializationStrategy == .activateAndFetchAsync { try confidence.activate() eventHandler.send(.ready) - confidence.asyncFetch() + confidence.asyncFetch(isProvider: true) } else { Task { - try await confidence.fetchAndActivate() + try await confidence.fetchAndActivate(isProvider: true) eventHandler.send(.ready) } } @@ -85,37 +85,40 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func updateConfidenceContext(context: EvaluationContext, removedKeys: [String] = []) { - confidence.putContext(context: ConfidenceTypeMapper.from(ctx: context), removeKeys: removedKeys) + confidence.putContext( + context: ConfidenceTypeMapper.from(ctx: context), + removeKeys: removedKeys, + isProvider: true) } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation() } public func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation() } public func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation() } public func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation() } public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: EvaluationContext?) throws -> OpenFeature.ProviderEvaluation { - try confidence.getEvaluation(key: key, defaultValue: defaultValue).toProviderEvaluation() + try confidence.getEvaluation(key: key, defaultValue: defaultValue, isProvider: true).toProviderEvaluation() } public func observe() -> AnyPublisher { diff --git a/Tests/ConfidenceTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceTests/ConfidenceFeatureProviderTest.swift index d400e83c..ace91d94 100644 --- a/Tests/ConfidenceTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceTests/ConfidenceFeatureProviderTest.swift @@ -52,7 +52,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { super.init(invocation: nil) // Workaround to use expectations in FakeClient } - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { callCount += 1 switch callCount { case 1: @@ -114,7 +114,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -172,7 +172,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -214,7 +214,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -254,7 +254,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -299,7 +299,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: XCTestCase, ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { if self.resolveStats == 1 { let expectation = expectation(description: "never fullfil") await fulfillment(of: [expectation]) @@ -345,7 +345,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -386,7 +386,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -427,7 +427,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -468,7 +468,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -509,7 +509,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } @@ -540,7 +540,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { class FakeClient: ConfidenceResolveClient { var resolveStats: Int = 0 var resolvedValues: [ResolvedValue] = [] - func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) async throws -> ResolvesResult { self.resolveStats += 1 return .init(resolvedValues: resolvedValues, resolveToken: "token") } diff --git a/Tests/ConfidenceTests/Helpers/ClientMock.swift b/Tests/ConfidenceTests/Helpers/ClientMock.swift index 659d4c2a..2a05216e 100644 --- a/Tests/ConfidenceTests/Helpers/ClientMock.swift +++ b/Tests/ConfidenceTests/Helpers/ClientMock.swift @@ -17,7 +17,7 @@ class ClientMock: ConfidenceResolveClient { self.testMode = testMode } - func resolve(ctx: ConfidenceStruct) throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct, isProvider: Bool) throws -> ResolvesResult { return ResolvesResult(resolvedValues: [], resolveToken: "") } diff --git a/Tests/ConfidenceTests/LocalStorageResolverTest.swift b/Tests/ConfidenceTests/LocalStorageResolverTest.swift index 85bf5f6b..d81fe6c6 100644 --- a/Tests/ConfidenceTests/LocalStorageResolverTest.swift +++ b/Tests/ConfidenceTests/LocalStorageResolverTest.swift @@ -18,7 +18,7 @@ class LocalStorageResolverTest: XCTestCase { XCTAssertNoThrow( try flagResolution.evaluate( - flagName: "flag_name.string", defaultValue: "default", context: [:]) + flagName: "flag_name.string", defaultValue: "default", context: [:], isProvider: false) ) } @@ -33,7 +33,11 @@ class LocalStorageResolverTest: XCTestCase { let flagResolution = FlagResolution(context: context, flags: [resolvedValue], resolveToken: "") XCTAssertThrowsError( - try flagResolution.evaluate(flagName: "new_flag_name", defaultValue: "default", context: context) + try flagResolution.evaluate( + flagName: "new_flag_name", + defaultValue: "default", + context: context, + isProvider: false) ) { error in XCTAssertEqual( error as? ConfidenceError, .flagNotFoundError(key: "new_flag_name")) diff --git a/Tests/ConfidenceTests/RemoteResolveConfidenceClientTest.swift b/Tests/ConfidenceTests/RemoteResolveConfidenceClientTest.swift index 16519b27..503d3b10 100644 --- a/Tests/ConfidenceTests/RemoteResolveConfidenceClientTest.swift +++ b/Tests/ConfidenceTests/RemoteResolveConfidenceClientTest.swift @@ -34,7 +34,7 @@ class RemoteResolveConfidenceClientTest: XCTestCase { let context = ["targeting_key": ConfidenceValue(string: "user1")] - let result = try await client.resolve(ctx: context) + let result = try await client.resolve(ctx: context, isProvider: false) XCTAssertEqual(result.resolvedValues.count, 2) let sortedResultValues = result.resolvedValues.sorted { resolvedValue1, resolvedValue2 in resolvedValue1.flag < resolvedValue2.flag