diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 1193f4b4..393fbc2a 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -13,6 +13,7 @@ public enum ErrorCode { case invalidContext case flagNotFound case evaluationError + case typeMismatch } struct FlagResolution: Encodable, Decodable, Equatable { @@ -68,20 +69,41 @@ extension FlagResolution { } let parsedValue = try getValue(path: parsedKey.path, value: value) - let pathValue: T = getTyped(value: parsedValue) ?? defaultValue + let typedValue: T? = getTyped(value: parsedValue) 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 - ) + if let typedValue = typedValue { + return Evaluation( + value: typedValue, + variant: resolvedFlag.variant, + reason: resolveReason, + errorCode: nil, + errorMessage: nil + ) + } else { + // `null` type from backend instructs to use client-side default value + if parsedValue == .init(null: ()) { + return Evaluation( + value: defaultValue, + variant: resolvedFlag.variant, + reason: resolveReason, + errorCode: nil, + errorMessage: nil + ) + } else { + return Evaluation( + value: defaultValue, + variant: nil, + reason: .error, + errorCode: .typeMismatch, + errorMessage: nil + ) + } + } } else { return Evaluation( value: defaultValue, diff --git a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift index b20960cc..538518a3 100644 --- a/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift +++ b/Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift @@ -145,6 +145,8 @@ extension Evaluation { throw OpenFeatureError.flagNotFoundError(key: self.errorMessage ?? "unknown key") case .evaluationError: throw OpenFeatureError.generalError(message: self.errorMessage ?? "unknown error") + case .typeMismatch: + throw OpenFeatureError.typeMismatchError } } return ProviderEvaluation( diff --git a/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift b/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift index c3f2c096..8c949b6c 100644 --- a/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift +++ b/Tests/ConfidenceTests/ConfidenceIntegrationTest.swift @@ -30,7 +30,7 @@ class ConfidenceIntegrationTests: XCTestCase { .withContext(initialContext: ctx) .build() try await confidence.fetchAndActivate() - let intResult = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: "1") + let intResult = confidence.getEvaluation(key: "\(resolveFlag).my-integer", defaultValue: 1) let boolResult = confidence.getEvaluation(key: "\(resolveFlag).my-boolean", defaultValue: false) diff --git a/Tests/ConfidenceTests/ConfidenceTest.swift b/Tests/ConfidenceTests/ConfidenceTest.swift index b5e83780..4d8c3f96 100644 --- a/Tests/ConfidenceTests/ConfidenceTest.swift +++ b/Tests/ConfidenceTests/ConfidenceTest.swift @@ -618,6 +618,44 @@ class ConfidenceTest: XCTestCase { } } + func testTypeMismatch() 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", + value: .init(structure: ["size": .init(boolean: true)]), + flag: "flag", + resolveReason: .match) + ] + + let confidence = Confidence.Builder(clientSecret: "test") + .withContext(initialContext: ["targeting_key": .init(string: "user1")]) + .withFlagResolverClient(flagResolver: client) + .withFlagApplier(flagApplier: flagApplier) + .build() + + try await confidence.fetchAndActivate() + let evaluation = confidence.getEvaluation( + key: "flag.size", + defaultValue: 1) + + XCTAssertEqual(client.resolveStats, 1) + XCTAssertEqual(evaluation.value, 1) + XCTAssertEqual(evaluation.errorCode, .typeMismatch) + XCTAssertNil(evaluation.errorMessage, "") + XCTAssertEqual(evaluation.reason, .error) + XCTAssertEqual(evaluation.variant, nil) + } + func testConcurrentActivate() async { for _ in 1...100 { Task {