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

refactor!: getEvaluation doesnt throw #158

Merged
merged 3 commits into from
Jul 10, 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
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
Loading