Skip to content

Commit

Permalink
refactor!: getEvaluation doesnt throw (#158)
Browse files Browse the repository at this point in the history
* refactor!: getEvaluation doesnt throw

* chore: update api spec file

* fix(provider): throw OpenFeature exceptions on failed evaluations

---------

Co-authored-by: Nicklas Lundin <[email protected]>
  • Loading branch information
fabriziodemaria and nicklasl authored Jul 10, 2024
1 parent 6bb03d5 commit 09f6b73
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 108 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ In the case of an error, the default value will be returned and the `Evaluation`

```swift
let message: String = confidence.getValue(key: "flag-name.message", defaultValue: "default message")
let messageFlag: Evaluation<String> = try confidence.getEvaluation(key: "flag-name.message", defaultValue: "default message")
let messageFlag: Evaluation<String> = confidence.getEvaluation(key: "flag-name.message", defaultValue: "default message")

let messageValue = messageFlag.value
// message and messageValue are the same
Expand Down
10 changes: 3 additions & 7 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ public class Confidence: ConfidenceEventSender {
}
}

public func getEvaluation<T>(key: String, defaultValue: T) throws -> Evaluation<T> {
try self.cache.evaluate(
public func getEvaluation<T>(key: String, defaultValue: T) -> Evaluation<T> {
self.cache.evaluate(
flagName: key,
defaultValue: defaultValue,
context: getContext(),
Expand All @@ -136,11 +136,7 @@ public class Confidence: ConfidenceEventSender {
}

public func getValue<T>(key: String, defaultValue: T) -> T {
do {
return try getEvaluation(key: key, defaultValue: defaultValue).value
} catch {
return defaultValue
}
return getEvaluation(key: key, defaultValue: defaultValue).value
}

func isStorageEmpty() -> Bool {
Expand Down
115 changes: 66 additions & 49 deletions Sources/Confidence/FlagEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public struct Evaluation<T> {
public enum ErrorCode {
case providerNotReady
case invalidContext
case flagNotFound
case evaluationError
}

struct FlagResolution: Encodable, Decodable, Equatable {
Expand All @@ -21,70 +23,85 @@ struct FlagResolution: Encodable, Decodable, Equatable {
}

extension FlagResolution {
// swiftlint:disable function_body_length
func evaluate<T>(
flagName: String,
defaultValue: T,
context: ConfidenceStruct,
flagApplier: FlagApplier? = nil
) throws -> Evaluation<T> {
let parsedKey = try FlagPath.getPath(for: flagName)
if self == FlagResolution.EMPTY {
throw ConfidenceError.flagNotFoundError(key: parsedKey.flag)
}
let resolvedFlag = self.flags.first { resolvedFlag in resolvedFlag.flag == parsedKey.flag }
guard let resolvedFlag = resolvedFlag else {
throw ConfidenceError.flagNotFoundError(key: parsedKey.flag)
}

if resolvedFlag.resolveReason != .targetingKeyError {
Task {
await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken)
) -> Evaluation<T> {
do {
let parsedKey = try FlagPath.getPath(for: flagName)
let resolvedFlag = self.flags.first { resolvedFlag in resolvedFlag.flag == parsedKey.flag }
guard let resolvedFlag = resolvedFlag else {
return Evaluation(
value: defaultValue,
variant: nil,
reason: .error,
errorCode: .flagNotFound,
errorMessage: "Flag '\(parsedKey.flag)' not found in local cache"
)
}
} else {
return Evaluation(
value: defaultValue,
variant: nil,
reason: .targetingKeyError,
errorCode: .invalidContext,
errorMessage: "Invalid targeting key"
)
}

guard let value = resolvedFlag.value else {
return Evaluation(
value: defaultValue,
variant: resolvedFlag.variant,
reason: resolvedFlag.resolveReason,
errorCode: nil,
errorMessage: nil
)
}
if resolvedFlag.resolveReason != .targetingKeyError {
Task {
await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken)
}
} else {
return Evaluation(
value: defaultValue,
variant: nil,
reason: .targetingKeyError,
errorCode: .invalidContext,
errorMessage: "Invalid targeting key"
)
}

let parsedValue = try getValue(path: parsedKey.path, value: value)
let pathValue: T = getTyped(value: parsedValue) ?? defaultValue
guard let value = resolvedFlag.value else {
return Evaluation(
value: defaultValue,
variant: resolvedFlag.variant,
reason: resolvedFlag.resolveReason,
errorCode: nil,
errorMessage: nil
)
}

if resolvedFlag.resolveReason == .match {
var resolveReason: ResolveReason = .match
if self.context != context {
resolveReason = .stale
let parsedValue = try getValue(path: parsedKey.path, value: value)
let pathValue: T = getTyped(value: parsedValue) ?? defaultValue

if resolvedFlag.resolveReason == .match {
var resolveReason: ResolveReason = .match
if self.context != context {
resolveReason = .stale
}
return Evaluation(
value: pathValue,
variant: resolvedFlag.variant,
reason: resolveReason,
errorCode: nil,
errorMessage: nil
)
} else {
return Evaluation(
value: defaultValue,
variant: resolvedFlag.variant,
reason: resolvedFlag.resolveReason,
errorCode: nil,
errorMessage: nil
)
}
return Evaluation(
value: pathValue,
variant: resolvedFlag.variant,
reason: resolveReason,
errorCode: nil,
errorMessage: nil
)
} else {
} catch {
return Evaluation(
value: defaultValue,
variant: resolvedFlag.variant,
reason: resolvedFlag.resolveReason,
errorCode: nil,
errorMessage: nil
variant: nil,
reason: .error,
errorCode: .evaluationError,
errorMessage: error.localizedDescription
)
}
}
// swiftlint:enable function_body_length

// swiftlint:disable:next cyclomatic_complexity
private func getTyped<T>(value: ConfidenceValue) -> T? {
Expand Down
16 changes: 14 additions & 2 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,20 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}

extension Evaluation {
func toProviderEvaluation() -> ProviderEvaluation<T> {
ProviderEvaluation(
func toProviderEvaluation() throws -> ProviderEvaluation<T> {
if let errorCode = self.errorCode {
switch errorCode {
case .providerNotReady:
throw OpenFeatureError.providerNotReadyError
case .invalidContext:
throw OpenFeatureError.invalidContextError
case .flagNotFound:
throw OpenFeatureError.flagNotFoundError(key: self.errorMessage ?? "unknown key")
case .evaluationError:
throw OpenFeatureError.generalError(message: self.errorMessage ?? "unknown error")
}
}
return ProviderEvaluation(
value: self.value,
variant: self.variant,
reason: self.reason.rawValue,
Expand Down
100 changes: 95 additions & 5 deletions Tests/ConfidenceProviderTests/ConfidenceProviderTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import XCTest
@testable import Confidence

class ConfidenceProviderTest: XCTestCase {
private var readyExpectation = XCTestExpectation(description: "Ready")
private var errorExpectation = XCTestExpectation(description: "Error")

func testErrorFetchOnInit() async throws {
let readyExpectation = XCTestExpectation(description: "Ready")
class FakeClient: ConfidenceResolveClient {
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
throw ConfidenceError.internalError(message: "test")
Expand All @@ -28,7 +26,7 @@ class ConfidenceProviderTest: XCTestCase {

let cancellable = OpenFeatureAPI.shared.observe().sink { event in
if event == .ready {
self.readyExpectation.fulfill()
readyExpectation.fulfill()
} else {
print(event)
}
Expand All @@ -38,6 +36,7 @@ class ConfidenceProviderTest: XCTestCase {
}

func testErrorStorageOnInit() async throws {
let errorExpectation = XCTestExpectation(description: "Error")
class FakeStorage: Storage {
func save(data: Encodable) throws {
// no-op
Expand Down Expand Up @@ -66,12 +65,103 @@ class ConfidenceProviderTest: XCTestCase {

let cancellable = OpenFeatureAPI.shared.observe().sink { event in
if event == .error {
self.errorExpectation.fulfill()
errorExpectation.fulfill()
} else {
print(event)
}
}
await fulfillment(of: [errorExpectation], timeout: 5.0)
cancellable.cancel()
}

func testProviderThrowsOpenFeatureErrors() async throws {
let context = MutableContext(targetingKey: "t")
let readyExpectation = XCTestExpectation(description: "Ready")
let storage = StorageMock()
class FakeClient: ConfidenceResolveClient {
var resolvedValues: [ResolvedValue] = [
ResolvedValue(
variant: "variant1",
value: .init(structure: ["int": .init(integer: 42)]),
flag: "flagName",
resolveReason: .match)
]
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
return .init(resolvedValues: resolvedValues, resolveToken: "token")
}
}

let confidence = Confidence.Builder(clientSecret: "test")
.withContext(initialContext: ["targeting_key": .init(string: "t")])
.withFlagResolverClient(flagResolver: FakeClient())
.withStorage(storage: storage)
.build()

let provider = ConfidenceFeatureProvider(confidence: confidence, initializationStrategy: .fetchAndActivate)
OpenFeatureAPI.shared.setProvider(provider: provider)
let cancellable = OpenFeatureAPI.shared.observe().sink { event in
if event == .ready {
readyExpectation.fulfill()
} else {
print(event)
}
}
await fulfillment(of: [readyExpectation], timeout: 1.0)
cancellable.cancel()
let evaluation = try provider.getIntegerEvaluation(key: "flagName.int", defaultValue: -1, context: context)
XCTAssertEqual(evaluation.value, 42)

XCTAssertThrowsError(try provider.getIntegerEvaluation(
key: "flagNotFound.something",
defaultValue: -1,
context: context))
{ error in
if let specificError = error as? OpenFeatureError {
XCTAssertEqual(specificError.errorCode(), ErrorCode.flagNotFound)
} else {
XCTFail("expected a flag not found error")
}
}
}
}

private class StorageMock: Storage {
var data = ""
var saveExpectation: XCTestExpectation?
private let storageQueue = DispatchQueue(label: "com.confidence.storagemock")

convenience init(data: Encodable) throws {
self.init()
try self.save(data: data)
}

func save(data: Encodable) throws {
try storageQueue.sync {
let dataB = try JSONEncoder().encode(data)
self.data = String(decoding: dataB, as: UTF8.self)

saveExpectation?.fulfill()
}
}

func load<T>(defaultValue: T) throws -> T where T: Decodable {
try storageQueue.sync {
if data.isEmpty {
return defaultValue
}
return try JSONDecoder().decode(T.self, from: try XCTUnwrap(data.data(using: .utf8)))
}
}

func clear() throws {
storageQueue.sync {
data = ""
}
}

func isEmpty() -> Bool {
storageQueue.sync {
return data.isEmpty
}
}
}
10 changes: 5 additions & 5 deletions Tests/ConfidenceTests/ConfidenceIntegrationTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class ConfidenceIntegrationTests: XCTestCase {
.withContext(initialContext: ctx)
.build()
try await confidence.fetchAndActivate()
let intResult = try confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: "1")
let boolResult = try confidence.getEvaluation(key: "\(resolveFlag).my-boolean", defaultValue: false)
let intResult = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: "1")
let boolResult = confidence.getEvaluation(key: "\(resolveFlag).my-boolean", defaultValue: false)


XCTAssertEqual(intResult.reason, .match)
Expand Down Expand Up @@ -104,7 +104,7 @@ class ConfidenceIntegrationTests: XCTestCase {
.build()
try await confidence.fetchAndActivate()

let result = try confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)
let result = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)

XCTAssertEqual(result.reason, .match)
XCTAssertNotNil(result.variant)
Expand Down Expand Up @@ -132,7 +132,7 @@ class ConfidenceIntegrationTests: XCTestCase {
.build()
try await confidence.fetchAndActivate()
// When evaluation of the flag happens using date context
let result = try confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)
let result = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)
// Then there is targeting match (non-default targeting)
XCTAssertEqual(result.reason, .match)
XCTAssertNotNil(result.variant)
Expand Down Expand Up @@ -162,7 +162,7 @@ class ConfidenceIntegrationTests: XCTestCase {
.build()
try await confidence.fetchAndActivate()
// When evaluation of the flag happens using date context
let result = try confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)
let result = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1)
// Then there is targeting match (non-default targeting)
XCTAssertEqual(result.value, 1)
XCTAssertEqual(result.reason, .noSegmentMatch)
Expand Down
Loading

0 comments on commit 09f6b73

Please sign in to comment.