From e061de262b6153ecb4dfd2f7ac899e6972af498c Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 14:53:09 +0200 Subject: [PATCH 01/14] add resolving against confidence context --- Sources/Confidence/ConfidenceValue.swift | 19 +++++ Sources/Confidence/ConfidenceValueHash.swift | 70 +++++++++++++++++++ .../ConfidenceClient/ConfidenceClient.swift | 3 +- .../RemoteResolveConfidenceClient.swift | 16 ++--- .../ConfidenceFeatureProvider.swift | 14 ++-- .../ConfidenceProvider/Utils/Extensions.swift | 4 +- .../ConfidenceFeatureProviderTest.swift | 2 +- .../Helpers/ClientMock.swift | 2 +- .../PersistentProviderCacheTest.swift | 6 +- .../RemoteResolveConfidenceClientTest.swift | 3 +- 10 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 Sources/Confidence/ConfidenceValueHash.swift diff --git a/Sources/Confidence/ConfidenceValue.swift b/Sources/Confidence/ConfidenceValue.swift index 01b399a3..15ff087d 100644 --- a/Sources/Confidence/ConfidenceValue.swift +++ b/Sources/Confidence/ConfidenceValue.swift @@ -3,6 +3,25 @@ import Common public typealias ConfidenceStruct = [String: ConfidenceValue] +public extension ConfidenceStruct { + func flattenOpenFeature() -> ConfidenceStruct { + var newStruct: ConfidenceStruct = [:] + let openFeatureStruct: ConfidenceValue? = self["open_feature"] + guard let openFeatureStruct: ConfidenceStruct = openFeatureStruct?.asStructure() else { + return self + } + // add open feature struct keys + for entry in openFeatureStruct { + newStruct[entry.key] = entry.value + } + // add all the rest keys + for entry in self { + newStruct[entry.key] = entry.value + } + return newStruct + } +} + public class ConfidenceValue: Equatable, Codable, CustomStringConvertible { private let value: ConfidenceValueInternal public var description: String { diff --git a/Sources/Confidence/ConfidenceValueHash.swift b/Sources/Confidence/ConfidenceValueHash.swift new file mode 100644 index 00000000..147f5787 --- /dev/null +++ b/Sources/Confidence/ConfidenceValueHash.swift @@ -0,0 +1,70 @@ +import CryptoKit +import Foundation + +public extension ConfidenceStruct { + func hash() -> String { + hashConfidenceValue(context: self) + } +} + +func hashConfidenceValue(context: ConfidenceStruct) -> String { + var hasher = SHA256() + + context.sorted { $0.key < $1.key }.forEach { key, value in + hasher.update(data: key.data) + hashValue(value: value, hasher: &hasher) + } + + let digest = hasher.finalize() + + return digest.map { String(format: "%02hhx", $0) }.joined() +} + +func hashValue(value: ConfidenceValue, hasher: inout some HashFunction) { + switch value.type() { + case .boolean: + hasher.update(data: value.asBoolean()!.data) + case .string: + hasher.update(data: value.asString()!.data) + case .integer: + hasher.update(data: value.asInteger()!.data) + case .double: + hasher.update(data: value.asDouble()!.data) + case .date: + hasher.update(data: value.asDate()!.data) + case .list: + value.asList()!.forEach { listValue in + hashValue(value: listValue, hasher: &hasher) + } + case .timestamp: + hasher.update(data: value.asDateComponents()!.date!.data) + case .structure: + value.asStructure()!.sorted { $0.key < $1.key }.forEach { key, structureValue in + hasher.update(data: key.data) + hashValue(value: structureValue, hasher: &hasher) + } + case .null: + hasher.update(data: UInt8(0).data) + } +} + +extension StringProtocol { + var data: Data { .init(utf8) } +} + +extension Numeric { + var data: Data { + var source = self + return .init(bytes: &source, count: MemoryLayout.size) + } +} + +extension Bool { + var data: Data { UInt8(self ? 1 : 0).data } +} + +extension Date { + var data: Data { + self.timeIntervalSince1970.data + } +} diff --git a/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift index 9cb6039e..e50bf1a6 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/ConfidenceClient.swift @@ -1,9 +1,10 @@ import Foundation +import Confidence import OpenFeature public protocol ConfidenceResolveClient { // Async - func resolve(ctx: EvaluationContext) async throws -> ResolvesResult + func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult } public struct ResolvedValue: Codable, Equatable { diff --git a/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift b/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift index d80bc5e5..ab22f8ce 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/RemoteResolveConfidenceClient.swift @@ -30,10 +30,10 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { // MARK: Resolver - public func resolve(flags: [String], ctx: EvaluationContext) async throws -> ResolvesResult { + public func resolve(flags: [String], ctx: ConfidenceStruct) async throws -> ResolvesResult { let request = ResolveFlagsRequest( flags: flags.map { "flags/\($0)" }, - evaluationContext: try getEvaluationContextStruct(ctx: ctx), + evaluationContext: try NetworkTypeMapper.from(value: ctx), clientSecret: options.credentials.getSecret(), apply: applyOnResolve, sdk: Sdk(id: metadata.name, version: metadata.version) @@ -51,7 +51,7 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { throw OpenFeatureError.parseError(message: "Unable to parse request response") } let resolvedValues = try response.resolvedFlags.map { resolvedFlag in - try convert(resolvedFlag: resolvedFlag, ctx: ctx) + try convert(resolvedFlag: resolvedFlag) } return ResolvesResult(resolvedValues: resolvedValues, resolveToken: response.resolveToken) case .failure(let errorData): @@ -60,13 +60,13 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { } } - public func resolve(ctx: EvaluationContext) async throws -> ResolvesResult { + public func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult { return try await resolve(flags: [], ctx: ctx) } // MARK: Private - private func convert(resolvedFlag: ResolvedFlag, ctx: EvaluationContext) throws -> ResolvedValue { + private func convert(resolvedFlag: ResolvedFlag) throws -> ResolvedValue { guard let responseFlagSchema = resolvedFlag.flagSchema, let responseValue = resolvedFlag.value, !responseValue.fields.isEmpty @@ -87,12 +87,6 @@ public class RemoteConfidenceResolveClient: ConfidenceResolveClient { resolveReason: convert(resolveReason: resolvedFlag.reason)) } - private func getEvaluationContextStruct(ctx: EvaluationContext) throws -> NetworkStruct { - var evaluationContext = TypeMapper.from(value: ctx) - evaluationContext.fields[targetingKey] = .string(ctx.getTargetingKey()) - return evaluationContext - } - private func handleError(error: Error) -> Error { if error is ConfidenceError || error is OpenFeatureError { return error diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 07960fce..aaa2fb41 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -85,11 +85,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { Task { do { - let resolveResult = try await resolve(context: initialContext) + let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: initialContext) + let resolveResult = try await resolve(context: context) // update cache with stored values try await store( - with: initialContext, + with: context, resolveResult: resolveResult, refreshCache: self.initializationStrategy == .fetchAndActivate ) @@ -106,7 +107,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func store( - with context: OpenFeature.EvaluationContext, + with context: ConfidenceStruct, resolveResult result: ResolvesResult, refreshCache: Bool ) async throws { @@ -133,10 +134,11 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.updateConfidenceContext(context: newContext) Task { do { - let resolveResult = try await resolve(context: newContext) + let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: newContext) + let resolveResult = try await resolve(context: context) // update the storage - try await store(with: newContext, resolveResult: resolveResult, refreshCache: true) + try await store(with: context, resolveResult: resolveResult, refreshCache: true) eventHandler.send(ProviderEvent.ready) } catch { @@ -220,7 +222,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } - private func resolve(context: OpenFeature.EvaluationContext) async throws -> ResolvesResult { + private func resolve(context: ConfidenceStruct) async throws -> ResolvesResult { do { let resolveResult = try await client.resolve(ctx: context) return resolveResult diff --git a/Sources/ConfidenceProvider/Utils/Extensions.swift b/Sources/ConfidenceProvider/Utils/Extensions.swift index 6a120375..3165b0af 100644 --- a/Sources/ConfidenceProvider/Utils/Extensions.swift +++ b/Sources/ConfidenceProvider/Utils/Extensions.swift @@ -1,8 +1,8 @@ import Foundation -import OpenFeature +import Confidence extension [ResolvedValue] { - func toCacheData(context: EvaluationContext, resolveToken: String) -> StoredCacheData { + func toCacheData(context: ConfidenceStruct, resolveToken: String) -> StoredCacheData { var cacheValues: [String: ResolvedValue] = [:] forEach { value in diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 1e04878b..40a1721d 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -323,7 +323,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { // Simulating a cache with an old evaluation context let data = [ResolvedValue(flag: "flag", resolveReason: .match)] - .toCacheData(context: MutableContext(targetingKey: "user0"), resolveToken: "token0") + .toCacheData(context: ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user0")), resolveToken: "token0") let storage = try StorageMock(data: data) diff --git a/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift b/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift index 8a4d653b..58f6caae 100644 --- a/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift +++ b/Tests/ConfidenceProviderTests/Helpers/ClientMock.swift @@ -20,7 +20,7 @@ class ClientMock: ConfidenceResolveClient { self.testMode = testMode } - func resolve(ctx: EvaluationContext) throws -> ResolvesResult { + func resolve(ctx: ConfidenceStruct) throws -> ResolvesResult { return ResolvesResult(resolvedValues: [], resolveToken: "") } diff --git a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift index bd74baef..dd082553 100644 --- a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift +++ b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift @@ -23,7 +23,7 @@ class PersistentProviderCacheTest: XCTestCase { flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ctx, resolveToken: resolveToken)) + try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx), resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) let cachedValue = try cache.getValue(flag: flag, ctx: ctx) @@ -48,7 +48,7 @@ class PersistentProviderCacheTest: XCTestCase { resolveReason: .match) XCTAssertFalse(try FileManager.default.fileExists(atPath: storage.getConfigUrl().backport.path)) - try storage.save(data: [value1, value2].toCacheData(context: ctx, resolveToken: resolveToken)) + try storage.save(data: [value1, value2].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx), resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) expectToEventually( @@ -85,7 +85,7 @@ class PersistentProviderCacheTest: XCTestCase { value: Value.double(3.14), flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ctx1, resolveToken: resolveToken)) + try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx1), resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) let cachedValue = try cache.getValue(flag: flag, ctx: ctx2) diff --git a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift index 19193e76..3de27b3d 100644 --- a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift +++ b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift @@ -1,5 +1,6 @@ import Foundation import OpenFeature +import Confidence import XCTest @testable import ConfidenceProvider @@ -34,7 +35,7 @@ class RemoteResolveConfidenceClientTest: XCTestCase { metadata: ConfidenceMetadata() ) - let result = try await client.resolve(ctx: MutableContext(targetingKey: "user1")) + let result = try await client.resolve(ctx: ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user1"))) XCTAssertEqual(result.resolvedValues.count, 2) let sortedResultValues = result.resolvedValues.sorted { resolvedValue1, resolvedValue2 in resolvedValue1.flag < resolvedValue2.flag From 1a89f668e34623c29733751bfe7831f4fc2e9478 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 15:01:30 +0200 Subject: [PATCH 02/14] send context hash to the provider cash --- .../Cache/InMemoryProviderCache.swift | 4 ++-- .../ConfidenceProvider/Cache/ProviderCache.swift | 2 +- .../ConfidenceClient/LocalStorageResolver.swift | 4 ++-- .../ConfidenceClient/Resolver.swift | 2 +- .../ConfidenceFeatureProvider.swift | 4 +++- .../Helpers/AlwaysFailCache.swift | 2 +- .../LocalStorageResolverTest.swift | 6 +++--- .../PersistentProviderCacheTest.swift | 13 ++++++++----- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift index 1573e5f3..78be56bd 100644 --- a/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift +++ b/Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift @@ -21,13 +21,13 @@ public class InMemoryProviderCache: ProviderCache { self.curEvalContextHash = curEvalContextHash } - public func getValue(flag: String, ctx: EvaluationContext) throws -> CacheGetValueResult? { + public func getValue(flag: String, contextHash: String) throws -> CacheGetValueResult? { if let value = self.cache[flag] { guard let curResolveToken = curResolveToken else { throw ConfidenceError.noResolveTokenFromCache } return .init( - resolvedValue: value, needsUpdate: curEvalContextHash != ctx.hash(), resolveToken: curResolveToken) + resolvedValue: value, needsUpdate: curEvalContextHash != contextHash, resolveToken: curResolveToken) } else { return nil } diff --git a/Sources/ConfidenceProvider/Cache/ProviderCache.swift b/Sources/ConfidenceProvider/Cache/ProviderCache.swift index da623458..c9d1feca 100644 --- a/Sources/ConfidenceProvider/Cache/ProviderCache.swift +++ b/Sources/ConfidenceProvider/Cache/ProviderCache.swift @@ -2,7 +2,7 @@ import Foundation import OpenFeature public protocol ProviderCache { - func getValue(flag: String, ctx: EvaluationContext) throws -> CacheGetValueResult? + func getValue(flag: String, contextHash: String) throws -> CacheGetValueResult? } public struct CacheGetValueResult { diff --git a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift index c3fb28aa..1354ee22 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/LocalStorageResolver.swift @@ -10,8 +10,8 @@ public class LocalStorageResolver: Resolver { self.cache = cache } - public func resolve(flag: String, ctx: EvaluationContext) throws -> ResolveResult { - let getResult = try self.cache.getValue(flag: flag, ctx: ctx) + public func resolve(flag: String, contextHash: String) throws -> ResolveResult { + let getResult = try self.cache.getValue(flag: flag, contextHash: contextHash) guard let getResult = getResult else { throw OpenFeatureError.flagNotFoundError(key: flag) } diff --git a/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift b/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift index 7cf3741f..cbd6ef52 100644 --- a/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift +++ b/Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift @@ -2,7 +2,7 @@ import OpenFeature public protocol Resolver { // This throws if the requested flag is not found - func resolve(flag: String, ctx: EvaluationContext) throws -> ResolveResult + func resolve(flag: String, contextHash: String) throws -> ResolveResult } public struct ResolveResult { diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index aaa2fb41..581eda3c 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -262,8 +262,10 @@ public class ConfidenceFeatureProvider: FeatureProvider { throw OpenFeatureError.invalidContextError } + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + do { - let resolverResult = try resolver.resolve(flag: path.flag, ctx: ctx) + let resolverResult = try resolver.resolve(flag: path.flag, contextHash: contextHash) guard let value = resolverResult.resolvedValue.value else { return resolveFlagNoValue( diff --git a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift index ac51f6e6..6e67ec97 100644 --- a/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift +++ b/Tests/ConfidenceProviderTests/Helpers/AlwaysFailCache.swift @@ -7,7 +7,7 @@ import OpenFeature public class AlwaysFailCache: ProviderCache { public func getValue( - flag: String, ctx: EvaluationContext + flag: String, contextHash: String ) throws -> CacheGetValueResult? { throw ConfidenceError.cacheError(message: "Always Fails (getValue)") } diff --git a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift index af9f27ef..9cf4234c 100644 --- a/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift +++ b/Tests/ConfidenceProviderTests/LocalStorageResolverTest.swift @@ -13,7 +13,7 @@ class LocalStorageResolverTest: XCTestCase { let ctx = MutableContext(targetingKey: "key", structure: MutableStructure()) XCTAssertThrowsError( - try resolver.resolve(flag: "test", ctx: ctx) + try resolver.resolve(flag: "test", contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) ) { error in XCTAssertEqual( error as? ConfidenceError, ConfidenceError.cachedValueExpired) @@ -26,7 +26,7 @@ class LocalStorageResolverTest: XCTestCase { let ctx = MutableContext(targetingKey: "key", structure: MutableStructure()) XCTAssertThrowsError( - try resolver.resolve(flag: "test", ctx: ctx) + try resolver.resolve(flag: "test", contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) ) { error in XCTAssertEqual( error as? OpenFeatureError, OpenFeatureError.flagNotFoundError(key: "test")) @@ -42,7 +42,7 @@ class TestCache: ProviderCache { self.returnType = returnType } - func getValue(flag: String, ctx: EvaluationContext) -> ConfidenceProvider.CacheGetValueResult? { + func getValue(flag: String, contextHash: String) -> ConfidenceProvider.CacheGetValueResult? { switch returnType { case .noValue: return nil diff --git a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift index dd082553..fa125646 100644 --- a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift +++ b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift @@ -26,7 +26,7 @@ class PersistentProviderCacheTest: XCTestCase { try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx), resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) - let cachedValue = try cache.getValue(flag: flag, ctx: ctx) + let cachedValue = try cache.getValue(flag: flag, contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) XCTAssertEqual(cachedValue?.resolvedValue, value) XCTAssertFalse(cachedValue?.needsUpdate ?? true) XCTAssertFalse(cachedValue?.needsUpdate ?? true) @@ -56,8 +56,9 @@ class PersistentProviderCacheTest: XCTestCase { let newCache = InMemoryProviderCache.from( storage: DefaultStorage(filePath: "resolver.flags.cache")) - let cachedValue1 = try newCache.getValue(flag: flag1, ctx: ctx) - let cachedValue2 = try newCache.getValue(flag: flag2, ctx: ctx) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + let cachedValue1 = try newCache.getValue(flag: flag1, contextHash: contextHash) + let cachedValue2 = try newCache.getValue(flag: flag2, contextHash: contextHash) XCTAssertEqual(cachedValue1?.resolvedValue, value1) XCTAssertEqual(cachedValue2?.resolvedValue, value2) XCTAssertEqual(cachedValue1?.needsUpdate, false) @@ -71,7 +72,8 @@ class PersistentProviderCacheTest: XCTestCase { try storage.clear() - let cachedValue = try cache.getValue(flag: "flag", ctx: ctx) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + let cachedValue = try cache.getValue(flag: "flag", contextHash: contextHash) XCTAssertNil(cachedValue?.resolvedValue.value) } @@ -88,7 +90,8 @@ class PersistentProviderCacheTest: XCTestCase { try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx1), resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) - let cachedValue = try cache.getValue(flag: flag, ctx: ctx2) + let contextHash = ConfidenceTypeMapper.from(ctx: ctx2).hash() + let cachedValue = try cache.getValue(flag: flag, contextHash: contextHash) XCTAssertEqual(cachedValue?.resolvedValue, value) XCTAssertTrue(cachedValue?.needsUpdate ?? false) } From f4d3545d0b549c2510e418520cb6e9dd814e3d5d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 15:03:38 +0200 Subject: [PATCH 03/14] evaluate against flatten of context --- Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 581eda3c..d106c8d5 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -262,7 +262,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { throw OpenFeatureError.invalidContextError } - let contextHash = ConfidenceTypeMapper.from(ctx: ctx).hash() + let contextHash = ConfidenceTypeMapper.from(ctx: ctx).flattenOpenFeature().hash() do { let resolverResult = try resolver.resolve(flag: path.flag, contextHash: contextHash) From d0753671a59ff47b659e0d1de6d5a6a1f692bc87 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 15:08:49 +0200 Subject: [PATCH 04/14] fixup! evaluate against flatten of context --- Sources/Confidence/ConfidenceValueHash.swift | 2 +- .../ConfidenceFeatureProvider.swift | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Sources/Confidence/ConfidenceValueHash.swift b/Sources/Confidence/ConfidenceValueHash.swift index 147f5787..705df1ee 100644 --- a/Sources/Confidence/ConfidenceValueHash.swift +++ b/Sources/Confidence/ConfidenceValueHash.swift @@ -37,7 +37,7 @@ func hashValue(value: ConfidenceValue, hasher: inout some HashFunction) { hashValue(value: listValue, hasher: &hasher) } case .timestamp: - hasher.update(data: value.asDateComponents()!.date!.data) + hasher.update(data: value.asDate()!.data) case .structure: value.asStructure()!.sorted { $0.key < $1.key }.forEach { key, structureValue in hasher.update(data: key.data) diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index d106c8d5..9c23d049 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -262,16 +262,16 @@ public class ConfidenceFeatureProvider: FeatureProvider { throw OpenFeatureError.invalidContextError } - let contextHash = ConfidenceTypeMapper.from(ctx: ctx).flattenOpenFeature().hash() + let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: ctx) do { - let resolverResult = try resolver.resolve(flag: path.flag, contextHash: contextHash) + let resolverResult = try resolver.resolve(flag: path.flag, contextHash: context.hash()) guard let value = resolverResult.resolvedValue.value else { return resolveFlagNoValue( defaultValue: defaultValue, resolverResult: resolverResult, - ctx: ctx + ctx: context ) } @@ -288,7 +288,6 @@ public class ConfidenceFeatureProvider: FeatureProvider { processResultForApply( resolverResult: resolverResult, - ctx: ctx, applyTime: Date.backport.now ) return evaluationResult @@ -303,14 +302,13 @@ public class ConfidenceFeatureProvider: FeatureProvider { } } - private func resolveFlagNoValue(defaultValue: T, resolverResult: ResolveResult, ctx: EvaluationContext) + private func resolveFlagNoValue(defaultValue: T, resolverResult: ResolveResult, ctx: ConfidenceStruct) -> ProviderEvaluation { switch resolverResult.resolvedValue.resolveReason { case .noMatch: processResultForApply( resolverResult: resolverResult, - ctx: ctx, applyTime: Date.backport.now) return ProviderEvaluation( value: defaultValue, @@ -403,7 +401,6 @@ public class ConfidenceFeatureProvider: FeatureProvider { private func processResultForApply( resolverResult: ResolveResult?, - ctx: OpenFeature.EvaluationContext?, applyTime: Date ) { guard let resolverResult = resolverResult, let resolveToken = resolverResult.resolveToken else { From 84bdd9fc1ecf1f70f7640daf9e883bcb06c4d7aa Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 15:43:33 +0200 Subject: [PATCH 05/14] use datecomponents for date --- Sources/Confidence/ConfidenceValueHash.swift | 37 +++++++++++++++---- .../ConfidenceFeatureProvider.swift | 6 ++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Sources/Confidence/ConfidenceValueHash.swift b/Sources/Confidence/ConfidenceValueHash.swift index 705df1ee..214e260b 100644 --- a/Sources/Confidence/ConfidenceValueHash.swift +++ b/Sources/Confidence/ConfidenceValueHash.swift @@ -20,29 +20,50 @@ func hashConfidenceValue(context: ConfidenceStruct) -> String { return digest.map { String(format: "%02hhx", $0) }.joined() } +// swiftlint:disable:next cyclomatic_complexity func hashValue(value: ConfidenceValue, hasher: inout some HashFunction) { switch value.type() { case .boolean: - hasher.update(data: value.asBoolean()!.data) + if let booleanData = value.asBoolean()?.data { + hasher.update(data: booleanData) + } + case .string: - hasher.update(data: value.asString()!.data) + if let stringData = value.asString()?.data { + hasher.update(data: stringData) + } + case .integer: - hasher.update(data: value.asInteger()!.data) + if let integerData = value.asInteger()?.data { + hasher.update(data: integerData) + } + case .double: - hasher.update(data: value.asDouble()!.data) + if let doubleData = value.asDouble()?.data { + hasher.update(data: doubleData) + } + case .date: - hasher.update(data: value.asDate()!.data) + if let dateData = value.asDateComponents()?.date?.data { + hasher.update(data: dateData) + } + case .list: - value.asList()!.forEach { listValue in + value.asList()?.forEach { listValue in hashValue(value: listValue, hasher: &hasher) } + case .timestamp: - hasher.update(data: value.asDate()!.data) + if let timestampData = value.asDate()?.data { + hasher.update(data: timestampData) + } + case .structure: - value.asStructure()!.sorted { $0.key < $1.key }.forEach { key, structureValue in + value.asStructure()?.sorted { $0.key < $1.key }.forEach { key, structureValue in hasher.update(data: key.data) hashValue(value: structureValue, hasher: &hasher) } + case .null: hasher.update(data: UInt8(0).data) } diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 9c23d049..1a54addf 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -85,7 +85,8 @@ public class ConfidenceFeatureProvider: FeatureProvider { Task { do { - let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: initialContext) + let context = confidence?.getContext() + .flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: initialContext) let resolveResult = try await resolve(context: context) // update cache with stored values @@ -134,7 +135,8 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.updateConfidenceContext(context: newContext) Task { do { - let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: newContext) + let context = confidence?.getContext() + .flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: newContext) let resolveResult = try await resolve(context: context) // update the storage From 26278f10c6a72c91f530bafe2e666cdfd33d67d4 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 16 Apr 2024 16:08:08 +0200 Subject: [PATCH 06/14] fix lint --- .../ConfidenceFeatureProvider.swift | 3 +- .../ConfidenceFeatureProviderTest.swift | 64 ++++++++++++++++++- .../MockedResolveClientURLProtocol.swift | 3 + .../PersistentProviderCacheTest.swift | 9 ++- .../RemoteResolveConfidenceClientTest.swift | 4 +- 5 files changed, 77 insertions(+), 6 deletions(-) diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 1a54addf..d9802f87 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -48,7 +48,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { } /// Initialize the Provider via a `Confidence` object. - public init(confidence: Confidence) { + public init(confidence: Confidence, session: URLSession? = nil) { let metadata = ConfidenceMetadata(version: "0.1.4") // x-release-please-version let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: confidence.clientSecret), @@ -65,6 +65,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { metadata: metadata) self.client = RemoteConfidenceResolveClient( options: options, + session: session, applyOnResolve: false, flagApplier: flagApplier, metadata: metadata) diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 40a1721d..87689217 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -310,6 +310,66 @@ class ConfidenceFeatureProviderTest: XCTestCase { } } + func testCreateProviderUsingConfidenceContextResolvesCorrectly() throws { + let resolve: [String: MockedResolveClientURLProtocol.ResolvedTestFlag] = [ + "user1": .init(variant: "control", value: .structure(["size": .integer(3)])) + ] + + let flags: [String: MockedResolveClientURLProtocol.TestFlag] = [ + "flags/flag": .init(resolve: resolve) + ] + + let session = MockedResolveClientURLProtocol.mockedSession(flags: flags) + + let confidence = Confidence + .Builder(clientSecret: "") + .build() + .withContext(["my_string": ConfidenceValue(string: "my_value")]) + + let provider = ConfidenceFeatureProvider(confidence: confidence, session: session) + + try withExtendedLifetime( + provider.observe().sink { event in + if event == .ready { + self.readyExpectation.fulfill() + } + }) + { + provider.initialize(initialContext: MutableContext(targetingKey: "user1")) + wait(for: [readyExpectation], timeout: 5) + + XCTAssertEqual(MockedResolveClientURLProtocol.resolveStats, 1) + XCTAssertTrue(MockedResolveClientURLProtocol + .resolveRequestFields.fields.contains { $0.key == "my_string" && $0.value == .string("my_value") } + ) + + XCTAssertTrue(MockedResolveClientURLProtocol + .resolveRequestFields.fields.contains { $0.key == "targeting_key" } + ) + + let requestTargetingKey = MockedResolveClientURLProtocol + .resolveRequestFields + .fields["targeting_key"] + + if case .string(let targetingKey) = requestTargetingKey { + XCTAssertTrue(!targetingKey.isEmpty) + } else { + XCTFail("targeting key could not be found") + } + + let evaluation = try provider.getIntegerEvaluation( + key: "flag.size", + defaultValue: 0, + context: MutableContext(targetingKey: "user1")) + + XCTAssertEqual(evaluation.value, 3) + XCTAssertNil(evaluation.errorCode) + XCTAssertNil(evaluation.errorMessage) + XCTAssertEqual(evaluation.reason, Reason.targetingMatch.rawValue) + XCTAssertEqual(evaluation.variant, "control") + } + } + func testStaleEvaluationContextInCache() throws { let resolve: [String: MockedResolveClientURLProtocol.ResolvedTestFlag] = [ "user1": .init(variant: "control", value: .structure(["size": .integer(3)])) @@ -322,8 +382,10 @@ class ConfidenceFeatureProviderTest: XCTestCase { let session = MockedResolveClientURLProtocol.mockedSession(flags: flags) // Simulating a cache with an old evaluation context + let context = ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user0")) + let data = [ResolvedValue(flag: "flag", resolveReason: .match)] - .toCacheData(context: ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user0")), resolveToken: "token0") + .toCacheData(context: context, resolveToken: "token0") let storage = try StorageMock(data: data) diff --git a/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift b/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift index a9f4b412..1fd87b1e 100644 --- a/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift +++ b/Tests/ConfidenceProviderTests/Helpers/MockedResolveClientURLProtocol.swift @@ -8,6 +8,7 @@ import OpenFeature class MockedResolveClientURLProtocol: URLProtocol { public static var callStats = 0 public static var resolveStats = 0 + public static var resolveRequestFields = NetworkStruct(fields: [:]) public static var flags: [String: TestFlag] = [:] public static var failFirstApply = false @@ -66,6 +67,8 @@ class MockedResolveClientURLProtocol: URLProtocol { return } + MockedResolveClientURLProtocol.resolveRequestFields = request.evaluationContext + guard case .string(let targetingKey) = request.evaluationContext.fields["targeting_key"] else { respondWithError( statusCode: 400, diff --git a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift index fa125646..1ca77bbe 100644 --- a/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift +++ b/Tests/ConfidenceProviderTests/PersistentProviderCacheTest.swift @@ -23,7 +23,8 @@ class PersistentProviderCacheTest: XCTestCase { flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx), resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx) + try storage.save(data: [value].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) let cachedValue = try cache.getValue(flag: flag, contextHash: ConfidenceTypeMapper.from(ctx: ctx).hash()) @@ -48,7 +49,8 @@ class PersistentProviderCacheTest: XCTestCase { resolveReason: .match) XCTAssertFalse(try FileManager.default.fileExists(atPath: storage.getConfigUrl().backport.path)) - try storage.save(data: [value1, value2].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx), resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx) + try storage.save(data: [value1, value2].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) expectToEventually( @@ -87,7 +89,8 @@ class PersistentProviderCacheTest: XCTestCase { value: Value.double(3.14), flag: flag, resolveReason: .match) - try storage.save(data: [value].toCacheData(context: ConfidenceTypeMapper.from(ctx: ctx1), resolveToken: resolveToken)) + let context = ConfidenceTypeMapper.from(ctx: ctx1) + try storage.save(data: [value].toCacheData(context: context, resolveToken: resolveToken)) cache = InMemoryProviderCache.from(storage: storage) let contextHash = ConfidenceTypeMapper.from(ctx: ctx2).hash() diff --git a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift index 3de27b3d..0fdfc1c0 100644 --- a/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift +++ b/Tests/ConfidenceProviderTests/RemoteResolveConfidenceClientTest.swift @@ -35,7 +35,9 @@ class RemoteResolveConfidenceClientTest: XCTestCase { metadata: ConfidenceMetadata() ) - let result = try await client.resolve(ctx: ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user1"))) + let context = ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user1")) + + let result = try await client.resolve(ctx: context) XCTAssertEqual(result.resolvedValues.count, 2) let sortedResultValues = result.resolvedValues.sorted { resolvedValue1, resolvedValue2 in resolvedValue1.flag < resolvedValue2.flag From 221d71fe02e688d30e5cee1ba59943547e1d951d Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 17 Apr 2024 16:01:21 +0200 Subject: [PATCH 07/14] hash only exists for confidence struct --- .../ConfidenceFeatureProvider.swift | 6 +- .../Utils/EvaluationContextHash.swift | 70 ------------------- .../EvaluationContextHashTest.swift | 31 -------- .../Helpers/StorageMock.swift | 2 +- .../ConfidenceValueHashTest.swift | 44 ++++++++++++ 5 files changed, 50 insertions(+), 103 deletions(-) delete mode 100644 Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift delete mode 100644 Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift create mode 100644 Tests/ConfidenceTests/ConfidenceValueHashTest.swift diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index d9802f87..5b86b9ab 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -129,7 +129,11 @@ public class ConfidenceFeatureProvider: FeatureProvider { oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext ) { - guard oldContext?.hash() != newContext.hash() else { + var oldConfidenceContext: ConfidenceStruct = [:] + if let context = oldContext { + oldConfidenceContext = ConfidenceTypeMapper.from(ctx: context) + } + guard oldConfidenceContext.hash() != ConfidenceTypeMapper.from(ctx: newContext).hash() else { return } diff --git a/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift b/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift deleted file mode 100644 index 52579a79..00000000 --- a/Sources/ConfidenceProvider/Utils/EvaluationContextHash.swift +++ /dev/null @@ -1,70 +0,0 @@ -import CryptoKit -import Foundation -import OpenFeature - -extension EvaluationContext { - func hash() -> String { - hashEvaluationContext(context: self) - } -} - -func hashEvaluationContext(context: EvaluationContext) -> String { - var hasher = SHA256() - - hasher.update(data: context.getTargetingKey().data) - context.asMap().sorted { $0.key < $1.key }.forEach { key, value in - hasher.update(data: key.data) - hashValue(value: value, hasher: &hasher) - } - - let digest = hasher.finalize() - - return digest.map { String(format: "%02hhx", $0) }.joined() -} - -func hashValue(value: Value, hasher: inout some HashFunction) { - switch value { - case .boolean(let bool): - hasher.update(data: bool.data) - case .string(let string): - hasher.update(data: string.data) - case .integer(let int64): - hasher.update(data: int64.data) - case .double(let double): - hasher.update(data: double.data) - case .date(let date): - hasher.update(data: date.data) - case .list(let list): - list.forEach { listValue in - hashValue(value: listValue, hasher: &hasher) - } - case .structure(let structure): - structure.sorted { $0.key < $1.key }.forEach { key, structureValue in - hasher.update(data: key.data) - hashValue(value: structureValue, hasher: &hasher) - } - case .null: - hasher.update(data: UInt8(0).data) - } -} - -extension StringProtocol { - var data: Data { .init(utf8) } -} - -extension Numeric { - var data: Data { - var source = self - return .init(bytes: &source, count: MemoryLayout.size) - } -} - -extension Bool { - var data: Data { UInt8(self ? 1 : 0).data } -} - -extension Date { - var data: Data { - self.timeIntervalSince1970.data - } -} diff --git a/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift b/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift deleted file mode 100644 index c60be68a..00000000 --- a/Tests/ConfidenceProviderTests/EvaluationContextHashTest.swift +++ /dev/null @@ -1,31 +0,0 @@ -import OpenFeature -import XCTest - -@testable import ConfidenceProvider - -final class MutableContextTests: XCTestCase { - func testHashRespectsTargetingKey() throws { - let ctx1 = MutableContext(targetingKey: "user1", structure: MutableStructure()) - let ctx2 = MutableContext(targetingKey: "user2", structure: MutableStructure()) - - XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) - } - - func testHashRespectsStructure() throws { - let ctx1 = MutableContext( - targetingKey: "", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - let ctx2 = MutableContext( - targetingKey: "", structure: MutableStructure(attributes: ["field": .list([.integer(4)])])) - - XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) - } - - func testHashIsEqualForEqualContext() throws { - let ctx1 = MutableContext( - targetingKey: "user1", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - let ctx2 = MutableContext( - targetingKey: "user1", structure: MutableStructure(attributes: ["field": .list([.integer(3)])])) - - XCTAssertEqual(ctx1.hash(), ctx2.hash()) - } -} diff --git a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift index bc98ec1f..a6276bc1 100644 --- a/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift +++ b/Tests/ConfidenceProviderTests/Helpers/StorageMock.swift @@ -28,7 +28,7 @@ class StorageMock: Storage { if data.isEmpty { return defaultValue } - return try JSONDecoder().decode(T.self, from: data.data) + return try JSONDecoder().decode(T.self, from: try XCTUnwrap(data.data(using: .utf8))) } } diff --git a/Tests/ConfidenceTests/ConfidenceValueHashTest.swift b/Tests/ConfidenceTests/ConfidenceValueHashTest.swift new file mode 100644 index 00000000..76184373 --- /dev/null +++ b/Tests/ConfidenceTests/ConfidenceValueHashTest.swift @@ -0,0 +1,44 @@ +import XCTest + +@testable import Confidence + +final class MutableContextTests: XCTestCase { + func testHashRespectsTargetingKey() throws { + let ctx1: ConfidenceStruct = + ["targetingKey": ConfidenceValue(string: "user1"), "structure": ConfidenceValue(structure: [:])] + let ctx2: ConfidenceStruct = + ["targetingKey": ConfidenceValue(string: "user2"), "structure": ConfidenceValue(structure: [:])] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } + + func testHashRespectsStructure() throws { + let ctx1: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user1"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + let ctx2: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user2"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 4)]) + ] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } + + func testHashIsEqualForEqualContext() throws { + let ctx1: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user1"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + let ctx2: ConfidenceStruct = + [ + "targetingKey": ConfidenceValue(string: "user2"), + "structure": ConfidenceValue(structure: ["integer": ConfidenceValue(integer: 3)]) + ] + + XCTAssertNotEqual(ctx1.hash(), ctx2.hash()) + } +} From 7bff0545174aa109d7344a0b5a1f60e5cde32b92 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 22 Apr 2024 13:44:55 +0200 Subject: [PATCH 08/14] Update Sources/Confidence/ConfidenceValue.swift Co-authored-by: Fabrizio Demaria --- Sources/Confidence/ConfidenceValue.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Confidence/ConfidenceValue.swift b/Sources/Confidence/ConfidenceValue.swift index 15ff087d..9b7e9abb 100644 --- a/Sources/Confidence/ConfidenceValue.swift +++ b/Sources/Confidence/ConfidenceValue.swift @@ -15,7 +15,7 @@ public extension ConfidenceStruct { newStruct[entry.key] = entry.value } // add all the rest keys - for entry in self { + for entry in self where entry.key != "open_feature" { newStruct[entry.key] = entry.value } return newStruct From dcc53b9a40946ac0b5f33dc30e2c6886d23fff49 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 23 Apr 2024 14:00:43 +0200 Subject: [PATCH 09/14] write open feature keys flatten and remove the flattening logi --- Sources/Confidence/ConfidenceValue.swift | 19 ------------------- .../ConfidenceFeatureProvider.swift | 17 +++++++++-------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/Sources/Confidence/ConfidenceValue.swift b/Sources/Confidence/ConfidenceValue.swift index 9b7e9abb..01b399a3 100644 --- a/Sources/Confidence/ConfidenceValue.swift +++ b/Sources/Confidence/ConfidenceValue.swift @@ -3,25 +3,6 @@ import Common public typealias ConfidenceStruct = [String: ConfidenceValue] -public extension ConfidenceStruct { - func flattenOpenFeature() -> ConfidenceStruct { - var newStruct: ConfidenceStruct = [:] - let openFeatureStruct: ConfidenceValue? = self["open_feature"] - guard let openFeatureStruct: ConfidenceStruct = openFeatureStruct?.asStructure() else { - return self - } - // add open feature struct keys - for entry in openFeatureStruct { - newStruct[entry.key] = entry.value - } - // add all the rest keys - for entry in self where entry.key != "open_feature" { - newStruct[entry.key] = entry.value - } - return newStruct - } -} - public class ConfidenceValue: Equatable, Codable, CustomStringConvertible { private let value: ConfidenceValueInternal public var description: String { diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index 5b86b9ab..f4369d37 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -86,8 +86,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { Task { do { - let context = confidence?.getContext() - .flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: initialContext) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: initialContext) let resolveResult = try await resolve(context: context) // update cache with stored values @@ -140,8 +139,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.updateConfidenceContext(context: newContext) Task { do { - let context = confidence?.getContext() - .flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: newContext) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: newContext) let resolveResult = try await resolve(context: context) // update the storage @@ -156,9 +154,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { } private func updateConfidenceContext(context: EvaluationContext) { - confidence?.updateContextEntry( - key: "open_feature", - value: ConfidenceValue(structure: ConfidenceTypeMapper.from(ctx: context))) + for entry in ConfidenceTypeMapper.from(ctx: context) { + confidence?.updateContextEntry( + key: entry.key, + value: entry.value + ) + } } public func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws @@ -269,7 +270,7 @@ public class ConfidenceFeatureProvider: FeatureProvider { throw OpenFeatureError.invalidContextError } - let context = confidence?.getContext().flattenOpenFeature() ?? ConfidenceTypeMapper.from(ctx: ctx) + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: ctx) do { let resolverResult = try resolver.resolve(flag: path.flag, contextHash: context.hash()) From eab3e2c6bffac551f8d8560baedb078c1b79db7f Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 23 Apr 2024 14:23:05 +0200 Subject: [PATCH 10/14] update test to remove the flattening logic --- .../ConfidenceFeatureProviderTest.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 87689217..95abcc2e 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -1005,11 +1005,9 @@ class ConfidenceFeatureProviderTest: XCTestCase { provider.onContextSet(oldContext: ctx1, newContext: ctx2) wait(for: [readyExpectation], timeout: 5) let context = confidence.getContext() - let expected = [ - "open_feature": ConfidenceValue(structure: [ - "targeting_key": ConfidenceValue(string: "user1"), - "active": ConfidenceValue(boolean: true) - ]) + let expected: ConfidenceStruct = [ + "targeting_key": ConfidenceValue(string: "user1"), + "active": ConfidenceValue(boolean: true) ] XCTAssertEqual(context, expected) } From 95a788209f01d2745f47674f172802a22ab7a006 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Tue, 23 Apr 2024 14:35:30 +0200 Subject: [PATCH 11/14] fixup! update test to remove the flattening logic --- .../ConfidenceFeatureProviderTest.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index 95abcc2e..2a6838a0 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -976,9 +976,7 @@ class ConfidenceFeatureProviderTest: XCTestCase { provider.initialize(initialContext: MutableContext(targetingKey: "user1")) wait(for: [readyExpectation], timeout: 5) let context = confidence.getContext() - let expected = [ - "open_feature": ConfidenceValue(structure: ["targeting_key": ConfidenceValue(string: "user1")]) - ] + let expected = ["targeting_key": ConfidenceValue(string: "user1")] XCTAssertEqual(context, expected) } } From d75654abb05cc689de126b846ab54a20c8ae4230 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Apr 2024 15:46:43 +0200 Subject: [PATCH 12/14] fixup! Merge branch 'main' into of-resolve-conf-context --- .../ConfidenceFeatureProvider.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index bfa9a076..b4af0cfe 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -265,16 +265,16 @@ public class ConfidenceFeatureProvider: FeatureProvider { variant: overrideValue.variant, reason: Reason.staticReason.rawValue) } - + guard let ctx = ctx else { throw OpenFeatureError.invalidContextError } - + let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: ctx) - + do { let resolverResult = try resolver.resolve(flag: path.flag, contextHash: context.hash()) - + guard let value = resolverResult.resolvedValue.value else { return resolveFlagNoValue( defaultValue: defaultValue, @@ -282,25 +282,25 @@ public class ConfidenceFeatureProvider: FeatureProvider { ctx: context ) } - + let pathValue: Value = try getValue(path: path.path, value: value) guard let typedValue: T = pathValue == .null ? defaultValue : pathValue.getTyped() else { throw OpenFeatureError.parseError(message: "Unable to parse flag value: \(pathValue)") } - + let isStale = resolverResult.resolvedValue.resolveReason == .stale let evaluationResult = ProviderEvaluation( value: typedValue, variant: resolverResult.resolvedValue.variant, reason: isStale ? Reason.stale.rawValue : Reason.targetingMatch.rawValue ) - + processResultForApply( resolverResult: resolverResult, applyTime: Date.backport.now ) - - + + return evaluationResult } } From c8b50335ae8944ec41c084961edaae936b17d34a Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Apr 2024 15:48:39 +0200 Subject: [PATCH 13/14] fixup! fixup! Merge branch 'main' into of-resolve-conf-context --- .../ConfidenceFeatureProviderTest.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift index e9ce8524..338834df 100644 --- a/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift +++ b/Tests/ConfidenceProviderTests/ConfidenceFeatureProviderTest.swift @@ -380,19 +380,10 @@ class ConfidenceFeatureProviderTest: XCTestCase { ] let session = MockedResolveClientURLProtocol.mockedSession(flags: flags) - // Simulating a cache with an old evaluation context - - let context = ConfidenceTypeMapper.from(ctx: MutableContext(targetingKey: "user0")) - - let data = [ResolvedValue(flag: "flag", resolveReason: .match)] - .toCacheData(context: context, resolveToken: "token0") - - let storage = try StorageMock(data: data) let provider = builder .with(session: session) - .with(storage: storage) .build() try withExtendedLifetime( provider.observe().sink { event in From 9755b5700bb0ed2c478d10b503703aa8bbf8a753 Mon Sep 17 00:00:00 2001 From: vahid torkaman Date: Wed, 24 Apr 2024 15:53:40 +0200 Subject: [PATCH 14/14] internal constructor --- Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index b4af0cfe..c719eb87 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -47,8 +47,12 @@ public class ConfidenceFeatureProvider: FeatureProvider { self.resolver = LocalStorageResolver(cache: cache) } + public convenience init(confidence: Confidence) { + self.init(confidence: confidence, session: nil) + } + /// Initialize the Provider via a `Confidence` object. - public init(confidence: Confidence, session: URLSession? = nil) { + internal init(confidence: Confidence, session: URLSession? = nil) { let metadata = ConfidenceMetadata(version: "0.1.4") // x-release-please-version let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: confidence.clientSecret),