Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add resolving against confidence context #94

Merged
merged 15 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
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
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)
fabriziodemaria marked this conversation as resolved.
Show resolved Hide resolved
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?,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup

applyTime: Date
) {
guard let resolverResult = resolverResult, let resolveToken = resolverResult.resolveToken else {
Expand Down
Loading
Loading