Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public class Confidence: ConfidenceEventSender {
- Parameter key:expects dot-notation to retrieve a specific entry in the flag's value, e.g. "flagname.myentry"
- Parameter defaultValue: returned in case of errors or in case of the variant's rule indicating to use the default value.
*/
public func getEvaluation<T>(key: String, defaultValue: T) -> Evaluation<T> {
public func getEvaluation<T: Decodable>(key: String, defaultValue: T) -> Evaluation<T> {
cacheQueue.sync { [weak self] in
guard let self = self else {
return Evaluation(
Expand All @@ -145,7 +145,7 @@ public class Confidence: ConfidenceEventSender {
- Parameter key:expects dot-notation to retrieve a specific entry in the flag's value, e.g. "flagname.myentry"
- Parameter defaultValue: returned in case of errors or in case of the variant's rule indicating to use the default value.
*/
public func getValue<T>(key: String, defaultValue: T) -> T {
public func getValue<T: Decodable>(key: String, defaultValue: T) -> T {
return getEvaluation(key: key, defaultValue: defaultValue).value
}

Expand Down
75 changes: 75 additions & 0 deletions Sources/Confidence/ConfidenceValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,81 @@ extension ConfidenceValue {
}
}

extension ConfidenceValue {
// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
// swiftlint:disable identifier_name
public func asJSONData() -> Data? {
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys

switch value {
case .boolean(let value):
return try? encoder.encode(value)
case .string(let value):
return try? encoder.encode(value)
case .integer(let value):
return try? encoder.encode(value)
case .double(let value):
return try? encoder.encode(value)
case .date(let value):
return try? encoder.encode(value)
case .timestamp(let value):
return try? encoder.encode(value)
case .structure(let values):
var flattened: [String: Any] = [:]
for (key, value) in values {
switch value {
case .boolean(let v): flattened[key] = v
case .string(let v): flattened[key] = v
case .integer(let v): flattened[key] = v
case .double(let v): flattened[key] = v
case .date(let v): flattened[key] = v
case .timestamp(let v): flattened[key] = v
case .structure(let v):
var nested: [String: Any] = [:]
for (nestedKey, nestedValue) in v {
switch nestedValue {
case .boolean(let v): nested[nestedKey] = v
case .string(let v): nested[nestedKey] = v
case .integer(let v): nested[nestedKey] = v
case .double(let v): nested[nestedKey] = v
case .date(let v): nested[nestedKey] = v
case .timestamp(let v): nested[nestedKey] = v
case .structure(let v):
var innerNested: [String: Any] = [:]
for (innerKey, innerValue) in v {
switch innerValue {
case .boolean(let v): innerNested[innerKey] = v
case .string(let v): innerNested[innerKey] = v
case .integer(let v): innerNested[innerKey] = v
case .double(let v): innerNested[innerKey] = v
case .date(let v): innerNested[innerKey] = v
case .timestamp(let v): innerNested[innerKey] = v
default: break
}
}
nested[nestedKey] = innerNested
default: break
}
}
flattened[key] = nested
default: break
}
}
return try? JSONSerialization.data(withJSONObject: flattened, options: .sortedKeys)
case .null:
return try? JSONSerialization.data(withJSONObject: NSNull())
default:
return nil
}
}
// swiftlint:enable function_body_length
// swiftlint:enable cyclomatic_complexity
// swiftlint:enable identifier_name
}


public enum ConfidenceValueType: CaseIterable {
case boolean
case string
Expand Down
46 changes: 39 additions & 7 deletions Sources/Confidence/FlagEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct FlagResolution: Encodable, Decodable, Equatable {
extension FlagResolution {
// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
func evaluate<T>(
func evaluate<T: Decodable>(
flagName: String,
defaultValue: T,
context: ConfidenceStruct,
Expand Down Expand Up @@ -55,7 +55,7 @@ extension FlagResolution {
}

guard let value = resolvedFlag.value else {
// No backend error, but nil value returned. This can happend with "noSegmentMatch" or "archived", for example
// No backend error, but nil value returned. This can happen with "noSegmentMatch" or "archived", for example
Task {
if resolvedFlag.shouldApply {
await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken)
Expand All @@ -71,7 +71,7 @@ extension FlagResolution {
}

let parsedValue = try getValue(path: parsedKey.path, value: value)
let typedValue: T? = getTyped(value: parsedValue)
let typedValue: T? = getTyped(value: parsedValue, debugLogger: debugLogger)

if resolvedFlag.resolveReason == .match {
var resolveReason: ResolveReason = .match
Expand Down Expand Up @@ -140,9 +140,9 @@ extension FlagResolution {
)
}
}

// swiftlint:enable function_body_length
// swiftlint:enable cyclomatic_complexity

private func checkBackendErrors<T>(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation<T>? {
if resolvedFlag.resolveReason == .targetingKeyError {
return Evaluation(
Expand All @@ -153,8 +153,8 @@ extension FlagResolution {
errorMessage: "Invalid targeting key"
)
} else if resolvedFlag.resolveReason == .error ||
resolvedFlag.resolveReason == .unknown ||
resolvedFlag.resolveReason == .unspecified {
resolvedFlag.resolveReason == .unknown ||
resolvedFlag.resolveReason == .unspecified {
return Evaluation(
value: defaultValue,
variant: nil,
Expand All @@ -168,7 +168,7 @@ extension FlagResolution {
}

// swiftlint:disable:next cyclomatic_complexity
private func getTyped<T>(value: ConfidenceValue) -> T? {
private func getTyped<T>(value: ConfidenceValue, debugLogger: DebugLogger?) -> T? {
if let value = self as? T {
return value
}
Expand Down Expand Up @@ -198,12 +198,44 @@ extension FlagResolution {
case .list:
return value.asList() as? T
case .structure:
// Try to decode as a Codable type if T is not ConfidenceStruct
if T.self != ConfidenceStruct.self {
return tryDecodeCodable(value: value, debugLogger: debugLogger)
}
return value.asStructure() as? T
case .null:
return nil
}
}

private func tryDecodeCodable<T>(value: ConfidenceValue, debugLogger: DebugLogger?) -> T? {
guard let decodable = T.self as? Decodable.Type else {
debugLogger?.logMessage(
message: "tryDecodeCodable: Type \(T.self) does not conform to Decodable",
isWarning: true)
return nil
}

guard let data = value.asJSONData() else {
debugLogger?.logMessage(
message: "tryDecodeCodable: Failed to encode ConfidenceValue to JSON",
isWarning: true)
return nil
}
do {
let decoded = try JSONDecoder().decode(decodable, from: data) as? T
if decoded == nil {
debugLogger?.logMessage(
message: "tryDecodeCodable: Failed to cast decoded value to type \(T.self)",
isWarning: true)
}
return decoded
} catch {
debugLogger?.logMessage(message: "tryDecodeCodable: Failed to decode JSON: \(error)", isWarning: true)
return nil
}
}

private func getValue(path: [String], value: ConfidenceValue) throws -> ConfidenceValue {
if path.isEmpty {
guard value.asStructure() != nil else {
Expand Down
90 changes: 89 additions & 1 deletion Tests/ConfidenceTests/ConfidenceTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,94 @@ class ConfidenceTest: XCTestCase {
XCTAssertEqual(flagApplier.applyCallCount, 1)
}

func testResolveObjectFlagWithUnderlyingStruct() async throws {
class FakeClient: ConfidenceResolveClient {
var resolveStats: Int = 0
var resolvedValues: [ResolvedValue] = []
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
self.resolveStats += 1
return .init(resolvedValues: resolvedValues, resolveToken: "token")
}
}

// swiftlint:disable:next line_length
let expected: ConfidenceValue = .init(structure: ["blob": .init(structure: ["size": .init(integer: 3), "name": .init(string: "testInner")]), "string": .init(string: "test")])
let client = FakeClient()
client.resolvedValues = [
ResolvedValue(
variant: "control",
value: expected,
flag: "flag",
resolveReason: .match,
shouldApply: false)
]

let confidence = Confidence.Builder(clientSecret: "test")
.withContext(initialContext: ["targeting_key": .init(string: "user2")])
.withFlagResolverClient(flagResolver: client)
.withFlagApplier(flagApplier: flagApplier)
.build()

try await confidence.fetchAndActivate()

// if the expected output is a struct, it's important the the defaultValue is ConfidenceStruct.
let defaultValue = ConfidenceStruct(uniqueKeysWithValues: [])
let evaluation = confidence.getValue(
key: "flag",
defaultValue: defaultValue)

XCTAssertEqual(evaluation, expected.asStructure())
}


func testResolveCodable() async throws {
class FakeClient: ConfidenceResolveClient {
var resolveStats: Int = 0
var resolvedValues: [ResolvedValue] = []
func resolve(ctx: ConfidenceStruct) async throws -> ResolvesResult {
self.resolveStats += 1
return .init(resolvedValues: resolvedValues, resolveToken: "token")
}
}

let client = FakeClient()
client.resolvedValues = [
ResolvedValue(
variant: "control",
// swiftlint:disable:next line_length
value: .init(structure: ["blob": .init(structure: ["size": .init(integer: 3), "name": .init(string: "testInner")]), "string": .init(string: "test")]),
flag: "flag",
resolveReason: .match,
shouldApply: false)
]

struct Blob: Codable {
let size: Int
let name: String
}

struct Flag: Codable {
let string: String
let blob: Blob
}

let confidence = Confidence.Builder(clientSecret: "test")
.withContext(initialContext: ["targeting_key": .init(string: "user2")])
.withFlagResolverClient(flagResolver: client)
.withFlagApplier(flagApplier: flagApplier)
.build()

try await confidence.fetchAndActivate()
let defaultValue = Flag(string: "", blob: Blob(size: 0, name: ""))
let evaluation = confidence.getValue(
key: "flag",
defaultValue: defaultValue)

let expected = Flag(string: "test", blob: Blob(size: 3, name: "testInner"))
XCTAssertEqual(evaluation.string, expected.string)
XCTAssertEqual(evaluation.blob.size, expected.blob.size)
XCTAssertEqual(evaluation.blob.name, expected.blob.name)
}

func testResolveAndApplyIntegerFlagNoSegmentMatch() async throws {
class FakeClient: ConfidenceResolveClient {
Expand Down Expand Up @@ -694,7 +782,7 @@ class ConfidenceTest: XCTestCase {
try await confidence.fetchAndActivate()
let evaluation = confidence.getEvaluation(
key: "flag.size",
defaultValue: [:])
defaultValue: ConfidenceStruct())

XCTAssertEqual(client.resolveStats, 1)
XCTAssertEqual(evaluation.value as? ConfidenceStruct, ["boolean": .init(boolean: true)])
Expand Down
15 changes: 15 additions & 0 deletions Tests/ConfidenceTests/ConfidenceValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,19 @@ final class ConfidenceConfidenceValueTests: XCTestCase {

XCTAssertEqual(value, decodedValue)
}

func testAsJSONData() throws {
let value = ConfidenceValue(structure: [
"field1": ConfidenceValue(integer: 3),
"field2": ConfidenceValue(string: "test"),
"field3": ConfidenceValue(structure: [
"field4": ConfidenceValue(integer: 4),
"field5": ConfidenceValue(string: "test2")
])
])
let data = try XCTUnwrap(value.asJSONData())
let dataAsString = try XCTUnwrap(String(data: data, encoding: .utf8))
// swiftlint:disable:next line_length
XCTAssertEqual(dataAsString, "{\"field1\":3,\"field2\":\"test\",\"field3\":{\"field4\":4,\"field5\":\"test2\"}}")
}
}
4 changes: 2 additions & 2 deletions api/Confidence_public_api.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
},
{
"name": "getEvaluation(key:defaultValue:)",
"declaration": "public func getEvaluation<T>(key: String, defaultValue: T) -> Evaluation<T>"
"declaration": "public func getEvaluation<T: Decodable>(key: String, defaultValue: T) -> Evaluation<T>"
},
{
"name": "getValue(key:defaultValue:)",
"declaration": "public func getValue<T>(key: String, defaultValue: T) -> T"
"declaration": "public func getValue<T: Decodable>(key: String, defaultValue: T) -> T"
},
{
"name": "getContext()",
Expand Down