Skip to content

Commit

Permalink
feat: Add resolving against confidence context (#94)
Browse files Browse the repository at this point in the history
* add resolving against confidence context

* send context hash to the provider cash

* evaluate against flatten of context

* fixup! evaluate against flatten of context

* use datecomponents for date

* fix lint

* hash only exists for confidence struct

* Update Sources/Confidence/ConfidenceValue.swift

Co-authored-by: Fabrizio Demaria <fabrizio.f.demaria@gmail.com>

* write open feature keys flatten and remove the flattening logi

* update test to remove the flattening logic

* fixup! update test to remove the flattening logic

* fixup! Merge branch 'main' into of-resolve-conf-context

* fixup! fixup! Merge branch 'main' into of-resolve-conf-context

* internal constructor

---------

Co-authored-by: Fabrizio Demaria <fabrizio.f.demaria@gmail.com>
  • Loading branch information
vahidlazio and fabriziodemaria committed Apr 24, 2024
1 parent 3c4febf commit a7cbb19
Show file tree
Hide file tree
Showing 20 changed files with 295 additions and 181 deletions.
91 changes: 91 additions & 0 deletions Sources/Confidence/ConfidenceValueHash.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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()
}

// swiftlint:disable:next cyclomatic_complexity
func hashValue(value: ConfidenceValue, hasher: inout some HashFunction) {
switch value.type() {
case .boolean:
if let booleanData = value.asBoolean()?.data {
hasher.update(data: booleanData)
}

case .string:
if let stringData = value.asString()?.data {
hasher.update(data: stringData)
}

case .integer:
if let integerData = value.asInteger()?.data {
hasher.update(data: integerData)
}

case .double:
if let doubleData = value.asDouble()?.data {
hasher.update(data: doubleData)
}

case .date:
if let dateData = value.asDateComponents()?.date?.data {
hasher.update(data: dateData)
}

case .list:
value.asList()?.forEach { listValue in
hashValue(value: listValue, hasher: &hasher)
}

case .timestamp:
if let timestampData = value.asDate()?.data {
hasher.update(data: timestampData)
}

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<Self>.size)
}
}

extension Bool {
var data: Data { UInt8(self ? 1 : 0).data }
}

extension Date {
var data: Data {
self.timeIntervalSince1970.data
}
}
4 changes: 2 additions & 2 deletions Sources/ConfidenceProvider/Cache/InMemoryProviderCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConfidenceProvider/Cache/ProviderCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/ConfidenceProvider/ConfidenceClient/Resolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
91 changes: 54 additions & 37 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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),
Expand All @@ -65,6 +69,7 @@ public class ConfidenceFeatureProvider: FeatureProvider {
metadata: metadata)
self.client = RemoteConfidenceResolveClient(
options: options,
session: session,
applyOnResolve: false,
flagApplier: flagApplier,
metadata: metadata)
Expand All @@ -85,11 +90,12 @@ public class ConfidenceFeatureProvider: FeatureProvider {

Task {
do {
let resolveResult = try await resolve(context: initialContext)
let context = confidence?.getContext() ?? 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
)
Expand All @@ -106,7 +112,7 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}

private func store(
with context: OpenFeature.EvaluationContext,
with context: ConfidenceStruct,
resolveResult result: ResolvesResult,
refreshCache: Bool
) async throws {
Expand All @@ -126,17 +132,22 @@ 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
}

self.updateConfidenceContext(context: newContext)
Task {
do {
let resolveResult = try await resolve(context: newContext)
let context = confidence?.getContext() ?? 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 {
Expand All @@ -147,9 +158,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
Expand Down Expand Up @@ -220,7 +234,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
Expand Down Expand Up @@ -260,44 +274,48 @@ public class ConfidenceFeatureProvider: FeatureProvider {
throw OpenFeatureError.invalidContextError
}

let resolverResult = try resolver.resolve(flag: path.flag, ctx: ctx)
let context = confidence?.getContext() ?? ConfidenceTypeMapper.from(ctx: ctx)

guard let value = resolverResult.resolvedValue.value else {
return resolveFlagNoValue(
defaultValue: defaultValue,
resolverResult: resolverResult,
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,
resolverResult: resolverResult,
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
)
}

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)")
}
processResultForApply(
resolverResult: resolverResult,
applyTime: Date.backport.now
)

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,
ctx: ctx,
applyTime: Date.backport.now
)
return evaluationResult
return evaluationResult
}
}

private func resolveFlagNoValue<T>(defaultValue: T, resolverResult: ResolveResult, ctx: EvaluationContext)
private func resolveFlagNoValue<T>(defaultValue: T, resolverResult: ResolveResult, ctx: ConfidenceStruct)
-> ProviderEvaluation<T>
{
switch resolverResult.resolvedValue.resolveReason {
case .noMatch:
processResultForApply(
resolverResult: resolverResult,
ctx: ctx,
applyTime: Date.backport.now)
return ProviderEvaluation(
value: defaultValue,
Expand Down Expand Up @@ -396,7 +414,6 @@ public class ConfidenceFeatureProvider: FeatureProvider {

private func processResultForApply(
resolverResult: ResolveResult?,
ctx: OpenFeature.EvaluationContext?,
applyTime: Date
) {
guard let resolverResult = resolverResult, let resolveToken = resolverResult.resolveToken else {
Expand Down
Loading

0 comments on commit a7cbb19

Please sign in to comment.