Sources/Confidence/FlagEvaluation.swift (202 lines of code) (raw):

import Foundation public struct Evaluation<T> { public let value: T public let variant: String? public let reason: ResolveReason public let errorCode: ErrorCode? public let errorMessage: String? } public enum ErrorCode { case providerNotReady case invalidContext case flagNotFound case evaluationError case typeMismatch } struct FlagResolution: Encodable, Decodable, Equatable { let context: ConfidenceStruct let flags: [ResolvedValue] let resolveToken: String static let EMPTY = FlagResolution(context: [:], flags: [], resolveToken: "") } extension FlagResolution { // swiftlint:disable function_body_length func evaluate<T>( flagName: String, defaultValue: T, context: ConfidenceStruct, flagApplier: FlagApplier? = nil, debugLogger: DebugLogger? = nil ) -> 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" ) } if let debugLogger = debugLogger { debugLogger.logResolveDebugURL(flagName: parsedKey.flag, context: context) } if let evaluation = checkBackendErrors(resolvedFlag: resolvedFlag, defaultValue: defaultValue) { return evaluation } guard let value = resolvedFlag.value else { // No backend error, but nil value returned. This can happend with "noSegmentMatch" or "archived", for example Task { await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, reason: resolvedFlag.resolveReason, errorCode: nil, errorMessage: nil ) } let parsedValue = try getValue(path: parsedKey.path, value: value) let typedValue: T? = getTyped(value: parsedValue) if resolvedFlag.resolveReason == .match { var resolveReason: ResolveReason = .match if self.context != context { resolveReason = .stale } if let typedValue = typedValue { Task { await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) } 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: ()) { Task { await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) } 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 { Task { await flagApplier?.apply(flagName: parsedKey.flag, resolveToken: self.resolveToken) } return Evaluation( value: defaultValue, variant: resolvedFlag.variant, reason: resolvedFlag.resolveReason, errorCode: nil, errorMessage: nil ) } } catch { return Evaluation( value: defaultValue, variant: nil, reason: .error, errorCode: .evaluationError, errorMessage: error.localizedDescription ) } } // swiftlint:enable function_body_length private func checkBackendErrors<T>(resolvedFlag: ResolvedValue, defaultValue: T) -> Evaluation<T>? { if resolvedFlag.resolveReason == .targetingKeyError { return Evaluation( value: defaultValue, variant: nil, reason: .targetingKeyError, errorCode: .invalidContext, errorMessage: "Invalid targeting key" ) } else if resolvedFlag.resolveReason == .error || resolvedFlag.resolveReason == .unknown || resolvedFlag.resolveReason == .unspecified { return Evaluation( value: defaultValue, variant: nil, reason: .error, errorCode: .evaluationError, errorMessage: "Unknown error from backend" ) } else { return nil } } // swiftlint:disable:next cyclomatic_complexity private func getTyped<T>(value: ConfidenceValue) -> T? { if let value = self as? T { return value } switch value.type() { case .boolean: return value.asBoolean() as? T case .string: return value.asString() as? T case .integer: if let intValue = value.asInteger() as? T { return intValue } if T.self == Int32.self, let intValue = value.asInteger() { return Int32(intValue) as? T } if T.self == Int64.self, let intValue = value.asInteger() { return Int64(intValue) as? T } return nil case .double: return value.asDouble() as? T case .date: return value.asDate() as? T case .timestamp: return value.asDateComponents() as? T case .list: return value.asList() as? T case .structure: return value.asStructure() as? T case .null: return nil } } private func getValue(path: [String], value: ConfidenceValue) throws -> ConfidenceValue { if path.isEmpty { guard value.asStructure() != nil else { throw ConfidenceError.parseError( message: "Flag path must contain path to the field for non-object values") } } var pathValue = value if !path.isEmpty { pathValue = try getValueForPath(path: path, value: value) } return pathValue } private func getValueForPath(path: [String], value: ConfidenceValue) throws -> ConfidenceValue { var curValue = value for field in path { guard let values = curValue.asStructure(), let newValue = values[field] else { throw ConfidenceError.internalError(message: "Unable to find key '\(field)'") } curValue = newValue } return curValue } }