diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index a1fdfa04..ba8ef5a7 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -211,6 +211,10 @@ A31088282837DCA900184942 /* ReferenceSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088252837DCA900184942 /* ReferenceSpec.swift */; }; A31088292837DCA900184942 /* KindSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31088262837DCA900184942 /* KindSpec.swift */; }; A33A5F7A28466D04000C29C7 /* LDContextStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A33A5F7928466D04000C29C7 /* LDContextStub.swift */; }; + A3470C372B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; }; + A3470C382B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; }; + A3470C392B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; }; + A3470C3A2B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */; }; A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */; }; A358D6D12A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; A358D6D22A4DD48600270C60 /* EnvironmentReporterChainBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */; }; @@ -256,6 +260,7 @@ A380B09A2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; }; A380B09B2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; }; A380B09C2B60178D00AB64A6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */; }; + A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */; }; B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */; }; B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4265EB024E7390C001CFD2C /* TestUtil.swift */; }; B468E71024B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; @@ -455,6 +460,7 @@ A31088252837DCA900184942 /* ReferenceSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferenceSpec.swift; sourceTree = ""; }; A31088262837DCA900184942 /* KindSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KindSpec.swift; sourceTree = ""; }; A33A5F7928466D04000C29C7 /* LDContextStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextStub.swift; sourceTree = ""; }; + A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoder.swift; sourceTree = ""; }; A3570F5928527B8200CF241A /* LDContextCodableSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDContextCodableSpec.swift; sourceTree = ""; }; A358D6D02A4DD48600270C60 /* EnvironmentReporterChainBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentReporterChainBase.swift; sourceTree = ""; }; A358D6D62A4DE6A500270C60 /* ApplicationInfoEnvironmentReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationInfoEnvironmentReporter.swift; sourceTree = ""; }; @@ -470,6 +476,7 @@ A36EDFCC2853C50B00D91B05 /* ObjcLDContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDContext.swift; sourceTree = ""; }; A3799D4429033665008D4A8E /* ObjcLDApplicationInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjcLDApplicationInfo.swift; sourceTree = ""; }; A380B0982B60178D00AB64A6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDValueDecoderSpec.swift; sourceTree = ""; }; B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; @@ -641,6 +648,7 @@ 83E2E2071F9FF9A0007514E9 /* Extensions */, 835E1D341F63332C00184DB4 /* ObjectiveC */, 83B6C4B71F4DE78B0055351C /* Support */, + A3470C362B7C1ACE00951CEE /* LDValueDecoder.swift */, ); name = LaunchDarkly; path = LaunchDarkly/LaunchDarkly; @@ -658,6 +666,7 @@ 83D17EA81FCDA16300B2823C /* Extensions */, 8354EFD21F22491C00C05156 /* Info.plist */, B4265EB024E7390C001CFD2C /* TestUtil.swift */, + A3FFE1122B7D4BA2009EF93F /* LDValueDecoderSpec.swift */, ); name = LaunchDarklyTests; path = LaunchDarkly/LaunchDarklyTests; @@ -1244,6 +1253,7 @@ 8311886C2113AE6400D77CB5 /* ObjcLDChangedFlag.swift in Sources */, C43C37E8238DF22D003C1624 /* LDEvaluationDetail.swift in Sources */, 8311884C2113ADDE00D77CB5 /* FlagChangeObserver.swift in Sources */, + A3470C3A2B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */, C443A41223186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831188592113AE1200D77CB5 /* FlagStore.swift in Sources */, C443A40D2315AA4D00145710 /* NetworkReporter.swift in Sources */, @@ -1311,6 +1321,7 @@ A31088192837DC0400184942 /* Reference.swift in Sources */, 831EF34E20655E730001C643 /* Event.swift in Sources */, A3799D4729033665008D4A8E /* ObjcLDApplicationInfo.swift in Sources */, + A3470C392B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */, C443A41123186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, 831EF35020655E730001C643 /* ClientServiceFactory.swift in Sources */, 831EF35120655E730001C643 /* KeyedValueCache.swift in Sources */, @@ -1377,6 +1388,7 @@ 835E1D431F685AC900184DB4 /* ObjcLDChangedFlag.swift in Sources */, 8358F25E1F474E5900ECE1AF /* LDChangedFlag.swift in Sources */, 83D559741FD87CC9002D10C8 /* KeyedValueCache.swift in Sources */, + A3470C372B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */, C43C37E1236BA050003C1624 /* LDEvaluationDetail.swift in Sources */, 831AAE2C20A9E4F600B46DBA /* Throttler.swift in Sources */, 8354EFE11F26380700C05156 /* LDConfig.swift in Sources */, @@ -1458,6 +1470,7 @@ 830DB3AC22380A3E00D65D25 /* HTTPHeadersSpec.swift in Sources */, 831425AF206ABB5300F2EF36 /* EnvironmentReportingMock.swift in Sources */, 838AB53F1F72A7D5006F03F5 /* FlagSynchronizerSpec.swift in Sources */, + A3FFE1132B7D4BA2009EF93F /* LDValueDecoderSpec.swift in Sources */, A3570F5A28527B8200CF241A /* LDContextCodableSpec.swift in Sources */, 837406D421F760640087B22B /* LDTimerSpec.swift in Sources */, 832307A61F7D8D720029815A /* URLRequestSpec.swift in Sources */, @@ -1493,6 +1506,7 @@ 83D9EC832062DEAB004D7FA6 /* KeyedValueCache.swift in Sources */, A358D6F02A4DE9EB00270C60 /* WatchOSEnvironmentReporter.swift in Sources */, 831AAE2D20A9E4F600B46DBA /* Throttler.swift in Sources */, + A3470C382B7C1ACE00951CEE /* LDValueDecoder.swift in Sources */, C43C37E6238DF22B003C1624 /* LDEvaluationDetail.swift in Sources */, 83D9EC872062DEAB004D7FA6 /* FlagSynchronizer.swift in Sources */, C443A41023186A4F00145710 /* ConnectionModeChangeObserver.swift in Sources */, diff --git a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift index 2814bc15..49168a9b 100644 --- a/LaunchDarkly/LaunchDarkly/LDClientVariation.swift +++ b/LaunchDarkly/LaunchDarkly/LDClientVariation.swift @@ -119,16 +119,44 @@ extension LDClient { variationDetailInternal(flagKey, defaultValue, needsReason: true) } - private func variationDetailInternal(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail { + /** + Returns the value of a feature flag for a given flag key, converting the raw JSON value into a type of your specification. + + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: the variation for the selected context, or `defaultValue` if the flag is not available. + */ + public func variation(forKey flagKey: LDFlagKey, defaultValue: T) -> T where T: LDValueConvertible, T: Decodable { + return variationDetailInternal(flagKey, defaultValue, needsReason: false).value + } + + /** + Returns the value of a feature flag for a given flag key, converting the raw JSON value into a type + of your specifification, and including it in an object that also describes the way the value was + determined. + + - parameter forKey: the unique feature key for the feature flag. + - parameter defaultValue: the default value for if the flag value is unavailable. + - returns: an `LDEvaluationDetail` object + */ + public func variationDetail(forKey flagKey: LDFlagKey, defaultValue: T) -> LDEvaluationDetail where T: LDValueConvertible, T: Decodable { + return variationDetailInternal(flagKey, defaultValue, needsReason: true) + } + + private func variationDetailInternal(_ flagKey: LDFlagKey, _ defaultValue: T, needsReason: Bool) -> LDEvaluationDetail where T: Decodable, T: LDValueConvertible { var result: LDEvaluationDetail let featureFlag = flagStore.featureFlag(for: flagKey) if let featureFlag = featureFlag { if featureFlag.value == .null { result = LDEvaluationDetail(value: defaultValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) - } else if let convertedValue = T(fromLDValue: featureFlag.value) { - result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) } else { - result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"]) + do { + let convertedValue = try LDValueDecoder().decode(T.self, from: featureFlag.value) + result = LDEvaluationDetail(value: convertedValue, variationIndex: featureFlag.variation, reason: featureFlag.reason) + } catch let error { + os_log("%s type conversion error %s: failed converting %s to type %s", log: config.logger, type: .debug, typeName(and: #function), String(describing: error), String(describing: featureFlag.value), String(describing: T.self)) + result = LDEvaluationDetail(value: defaultValue, variationIndex: nil, reason: ["kind": "ERROR", "errorKind": "WRONG_TYPE"]) + } } } else { os_log("%s Unknown feature flag %s; returning default value", log: config.logger, type: .debug, typeName(and: #function), flagKey.description) @@ -144,65 +172,48 @@ extension LDClient { } } -private protocol LDValueConvertible { - init?(fromLDValue: LDValue) +/** + Protocol indicting a type can be converted into an LDValue. + + Types used with the `LDClient.variation(forKey: defaultValue:)` or `LDClient.variationDetail(forKey: detailValue:)` + methods are required to implement this protocol. This protocol has already been implemented for Bool, Int, Double, String, + and LDValue types. + + This allows custom types as evaluation result types while retaining the LDValue type throughout the event processing system. + */ +public protocol LDValueConvertible { + /** + Return an LDValue representation of this instance. + */ func toLDValue() -> LDValue } extension Bool: LDValueConvertible { - init?(fromLDValue value: LDValue) { - guard case .bool(let value) = value - else { return nil } - self = value - } - - func toLDValue() -> LDValue { + public func toLDValue() -> LDValue { return .bool(self) } } extension Int: LDValueConvertible { - init?(fromLDValue value: LDValue) { - guard case .number(let value) = value, let intValue = Int(exactly: value.rounded()) - else { return nil } - self = intValue - } - - func toLDValue() -> LDValue { + public func toLDValue() -> LDValue { return .number(Double(self)) } } extension Double: LDValueConvertible { - init?(fromLDValue value: LDValue) { - guard case .number(let value) = value - else { return nil } - self = value - } - - func toLDValue() -> LDValue { + public func toLDValue() -> LDValue { return .number(self) } } extension String: LDValueConvertible { - init?(fromLDValue value: LDValue) { - guard case .string(let value) = value - else { return nil } - self = value - } - - func toLDValue() -> LDValue { + public func toLDValue() -> LDValue { return .string(self) } } extension LDValue: LDValueConvertible { - init?(fromLDValue value: LDValue) { - self = value - } - - func toLDValue() -> LDValue { + public func toLDValue() -> LDValue { return self } } diff --git a/LaunchDarkly/LaunchDarkly/LDValueDecoder.swift b/LaunchDarkly/LaunchDarkly/LDValueDecoder.swift new file mode 100644 index 00000000..b2bd7e7f --- /dev/null +++ b/LaunchDarkly/LaunchDarkly/LDValueDecoder.swift @@ -0,0 +1,755 @@ +import Foundation + +/** + This source file contains modified types and structures taken from the the Swift CoreLibs + Foundation GitHub repository. + + The types and code in this file were originally part of the `JSONDecoder` and `_JSONDecoderImpl` + implementations. The code has been updated to work with the LaunchDarkly LDValue type instead. + + The original source header and comments have been left in tact as much as possible. Some + modifications were required as part of the updates. + */ + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// Taken from https://github.com/apple/swift-corelibs-foundation/blob/dbca8c7ddcfd19f7f6f6e1b60fd3ee3f748e263c/Sources/Foundation/JSONEncoder.swift#L1186 + +internal struct LDValueKey: CodingKey { + public var stringValue: String + public var intValue: Int? + + public init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + public init?(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + public init(stringValue: String, intValue: Int?) { + self.stringValue = stringValue + self.intValue = intValue + } + + internal init(index: Int) { + self.stringValue = "Index \(index)" + self.intValue = index + } + + internal static let `super` = LDValueKey(stringValue: "super")! +} + +/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary` +/// containing `Decodable` values (in which case it should be exempt from key conversion strategies). +/// +/// The marker protocol also provides access to the type of the `Decodable` values, +/// which is needed for the implementation of the key conversion strategy exemption. +/// +fileprivate protocol _JSONStringDictionaryDecodableMarker { + static var elementType: Decodable.Type { get } +} + +extension Dictionary: _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable { + static var elementType: Decodable.Type { return Value.self } +} + +// Taken from https://github.com/apple/swift-corelibs-foundation/blob/dbca8c7ddcfd19f7f6f6e1b60fd3ee3f748e263c/Sources/Foundation/JSONDecoder.swift + +//===----------------------------------------------------------------------===// +// JSON Decoder +//===----------------------------------------------------------------------===// + +/// `LDValueDecoder` facilitates the decoding of LDValue into semantic `Decodable` types. +class LDValueDecoder { + /// Contextual user-provided information for use during decoding. + var userInfo: [CodingUserInfoKey: Any] = [:] + + /// Options set on the top-level encoder to pass down the decoding hierarchy. + fileprivate struct _Options { + let userInfo: [CodingUserInfoKey: Any] + } + + /// The options set on the top-level decoder. + fileprivate var options: _Options { + return _Options(userInfo: userInfo) + } + + // MARK: - Constructing a JSON Decoder + + /// Initializes `self` with default strategies. + public init() {} + + // MARK: - Decoding Values + + /// Decodes a top-level value of the given type from the given JSON representation. + /// + /// - parameter type: The type of the value to decode. + /// - parameter data: The data to decode from. + /// - returns: A value of the requested type. + /// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON. + /// - throws: An error if any value throws an error during decoding. + func decode(_ type: T.Type, from data: LDValue) throws -> T { + return try LDValueDecoderImpl(userInfo: self.userInfo, from: data, codingPath: [], options: self.options).unwrap(as: type) + } +} + +// MARK: - _LDValueDecoder + +fileprivate struct LDValueDecoderImpl { + let codingPath: [CodingKey] + let userInfo: [CodingUserInfoKey: Any] + + let json: LDValue + let options: LDValueDecoder._Options + + init(userInfo: [CodingUserInfoKey: Any], from json: LDValue, codingPath: [CodingKey], options: LDValueDecoder._Options) { + self.userInfo = userInfo + self.codingPath = codingPath + self.json = json + self.options = options + } +} + +extension LDValueDecoderImpl: Decoder { + @usableFromInline func container(keyedBy _: Key.Type) throws -> + KeyedDecodingContainer where Key: CodingKey { + switch self.json { + case .object(let dictionary): + let container = KeyedContainer( + impl: self, + codingPath: codingPath, + dictionary: dictionary + ) + return KeyedDecodingContainer(container) + case .null: + throw DecodingError.valueNotFound([String: LDValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead" + )) + default: + throw DecodingError.typeMismatch([String: LDValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Expected to decode \([String: LDValue].self) but found \(self.json) instead." + )) + } + } + + @usableFromInline func unkeyedContainer() throws -> UnkeyedDecodingContainer { + switch self.json { + case .array(let array): + return UnkeyedContainer( + impl: self, + codingPath: self.codingPath, + array: array + ) + case .null: + throw DecodingError.valueNotFound([String: LDValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null value instead" + )) + default: + throw DecodingError.typeMismatch([LDValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Expected to decode \([LDValue].self) but found \(self.json) instead." + )) + } + } + + @usableFromInline func singleValueContainer() throws -> SingleValueDecodingContainer { + SingleValueContainer( + impl: self, + codingPath: self.codingPath, + json: self.json + ) + } + + // MARK: Special case handling + + func unwrap(as type: T.Type) throws -> T { + if type == Date.self { + return try Date(from: self) as! T // swiftlint:disable:this force_cast + } + if type == Data.self { + return try Data(from: self) as! T // swiftlint:disable:this force_cast + } + if type == URL.self { + return try self.unwrapURL() as! T // swiftlint:disable:this force_cast + } + if type == Decimal.self { + return try self.unwrapDecimal() as! T // swiftlint:disable:this force_cast + } + if type is _JSONStringDictionaryDecodableMarker.Type { + return try self.unwrapDictionary(as: type) + } + + return try type.init(from: self) + } + + private func unwrapURL() throws -> URL { + let container = SingleValueContainer(impl: self, codingPath: self.codingPath, json: self.json) + let string = try container.decode(String.self) + + guard let url = URL(string: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, + debugDescription: "Invalid URL string.")) + } + return url + } + + private func unwrapDecimal() throws -> Decimal { + guard case .number(let asDouble) = self.json else { + throw DecodingError.typeMismatch(Decimal.self, DecodingError.Context(codingPath: self.codingPath, debugDescription: "")) + } + + return Decimal(floatLiteral: asDouble) + } + + private func unwrapDictionary(as: T.Type) throws -> T { + guard let dictType = T.self as? (_JSONStringDictionaryDecodableMarker & Decodable).Type else { + preconditionFailure("Must only be called of T implements _JSONStringDictionaryDecodableMarker") + } + + guard case .object(let object) = self.json else { + throw DecodingError.typeMismatch([String: LDValue].self, DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Expected to decode \([String: LDValue].self) but found \(self.json) instead." + )) + } + + var result = [String: Any]() + + for (key, value) in object { + var newPath = self.codingPath + newPath.append(LDValueKey(stringValue: key)!) + let newDecoder = LDValueDecoderImpl(userInfo: self.userInfo, from: value, codingPath: newPath, options: self.options) + + result[key] = try dictType.elementType.createByDirectlyUnwrapping(from: newDecoder) + } + + return result as! T // swiftlint:disable:this force_cast + } + + private func unwrapFloatingPoint( + from value: LDValue, + for additionalKey: CodingKey? = nil, + as type: T.Type) throws -> T + { + if case .number(let number) = value { + return T(number) + } + + throw self.createTypeMismatchError(type: T.self, for: additionalKey, value: value) + } + + private func unwrapFixedWidthInteger( + from value: LDValue, + for additionalKey: CodingKey? = nil, + as type: T.Type) throws -> T + { + guard case .number(let number) = value else { + throw self.createTypeMismatchError(type: T.self, for: additionalKey, value: value) + } + + return T(number) + } + + private func createTypeMismatchError(type: Any.Type, for additionalKey: CodingKey? = nil, value: LDValue) -> DecodingError { + var path = self.codingPath + if let additionalKey = additionalKey { + path.append(additionalKey) + } + + return DecodingError.typeMismatch(type, .init( + codingPath: path, + debugDescription: "Expected to decode \(type) but found \(value) instead." + )) + } +} + +extension Decodable { + fileprivate static func createByDirectlyUnwrapping(from decoder: LDValueDecoderImpl) throws -> Self { + if Self.self == URL.self + || Self.self == Date.self + || Self.self == Data.self + || Self.self == Decimal.self + || Self.self is _JSONStringDictionaryDecodableMarker.Type { + return try decoder.unwrap(as: Self.self) + } + + return try Self.init(from: decoder) + } +} + +extension LDValueDecoderImpl { + struct SingleValueContainer: SingleValueDecodingContainer { + let impl: LDValueDecoderImpl + let value: LDValue + let codingPath: [CodingKey] + + init(impl: LDValueDecoderImpl, codingPath: [CodingKey], json: LDValue) { + self.impl = impl + self.codingPath = codingPath + self.value = json + } + + func decodeNil() -> Bool { + self.value == .null + } + + func decode(_: Bool.Type) throws -> Bool { + guard case .bool(let bool) = self.value else { + throw self.impl.createTypeMismatchError(type: Bool.self, value: self.value) + } + + return bool + } + + func decode(_: String.Type) throws -> String { + guard case .string(let string) = self.value else { + throw self.impl.createTypeMismatchError(type: String.self, value: self.value) + } + + return string + } + + func decode(_: Double.Type) throws -> Double { + try decodeFloatingPoint() + } + + func decode(_: Float.Type) throws -> Float { + try decodeFloatingPoint() + } + + func decode(_: Int.Type) throws -> Int { + try decodeFixedWidthInteger() + } + + func decode(_: Int8.Type) throws -> Int8 { + try decodeFixedWidthInteger() + } + + func decode(_: Int16.Type) throws -> Int16 { + try decodeFixedWidthInteger() + } + + func decode(_: Int32.Type) throws -> Int32 { + try decodeFixedWidthInteger() + } + + func decode(_: Int64.Type) throws -> Int64 { + try decodeFixedWidthInteger() + } + + func decode(_: UInt.Type) throws -> UInt { + try decodeFixedWidthInteger() + } + + func decode(_: UInt8.Type) throws -> UInt8 { + try decodeFixedWidthInteger() + } + + func decode(_: UInt16.Type) throws -> UInt16 { + try decodeFixedWidthInteger() + } + + func decode(_: UInt32.Type) throws -> UInt32 { + try decodeFixedWidthInteger() + } + + func decode(_: UInt64.Type) throws -> UInt64 { + try decodeFixedWidthInteger() + } + + func decode(_ type: T.Type) throws -> T where T: Decodable { + try self.impl.unwrap(as: type) + } + + @inline(__always) private func decodeFixedWidthInteger() throws -> T { + try self.impl.unwrapFixedWidthInteger(from: self.value, as: T.self) + } + + @inline(__always) private func decodeFloatingPoint() throws -> T { + try self.impl.unwrapFloatingPoint(from: self.value, as: T.self) + } + } +} + +extension LDValueDecoderImpl { + struct KeyedContainer: KeyedDecodingContainerProtocol { + typealias Key = K + + let impl: LDValueDecoderImpl + let codingPath: [CodingKey] + let dictionary: [String: LDValue] + + init(impl: LDValueDecoderImpl, codingPath: [CodingKey], dictionary: [String: LDValue]) { + self.impl = impl + self.codingPath = codingPath + self.dictionary = dictionary + } + + var allKeys: [K] { + self.dictionary.keys.compactMap { K(stringValue: $0) } + } + + func contains(_ key: K) -> Bool { + if let _ = dictionary[key.stringValue] { + return true + } + return false + } + + func decodeNil(forKey key: K) throws -> Bool { + let value = try getValue(forKey: key) + return value == .null + } + + func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { + let value = try getValue(forKey: key) + + guard case .bool(let bool) = value else { + throw createTypeMismatchError(type: type, forKey: key, value: value) + } + + return bool + } + + func decode(_ type: String.Type, forKey key: K) throws -> String { + let value = try getValue(forKey: key) + + guard case .string(let string) = value else { + throw createTypeMismatchError(type: type, forKey: key, value: value) + } + + return string + } + + func decode(_: Double.Type, forKey key: K) throws -> Double { + try decodeFloatingPoint(key: key) + } + + func decode(_: Float.Type, forKey key: K) throws -> Float { + try decodeFloatingPoint(key: key) + } + + func decode(_: Int.Type, forKey key: K) throws -> Int { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: Int8.Type, forKey key: K) throws -> Int8 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: Int16.Type, forKey key: K) throws -> Int16 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: Int32.Type, forKey key: K) throws -> Int32 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: Int64.Type, forKey key: K) throws -> Int64 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: UInt.Type, forKey key: K) throws -> UInt { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: UInt8.Type, forKey key: K) throws -> UInt8 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: UInt16.Type, forKey key: K) throws -> UInt16 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: UInt32.Type, forKey key: K) throws -> UInt32 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_: UInt64.Type, forKey key: K) throws -> UInt64 { + try decodeFixedWidthInteger(key: key) + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T: Decodable { + let newDecoder = try decoderForKey(key) + return try newDecoder.unwrap(as: type) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws + -> KeyedDecodingContainer where NestedKey: CodingKey + { + try decoderForKey(key).container(keyedBy: type) + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + try decoderForKey(key).unkeyedContainer() + } + + func superDecoder() throws -> Decoder { + return decoderForKeyNoThrow(LDValueKey.super) + } + + func superDecoder(forKey key: K) throws -> Decoder { + return decoderForKeyNoThrow(key) + } + + private func decoderForKey(_ key: LocalKey) throws -> LDValueDecoderImpl { + let value = try getValue(forKey: key) + var newPath = self.codingPath + newPath.append(key) + + return LDValueDecoderImpl( + userInfo: self.impl.userInfo, + from: value, + codingPath: newPath, + options: self.impl.options + ) + } + + private func decoderForKeyNoThrow(_ key: LocalKey) -> LDValueDecoderImpl { + let value: LDValue + do { + value = try getValue(forKey: key) + } catch { + // if there no value for this key then return a null value + value = .null + } + var newPath = self.codingPath + newPath.append(key) + + return LDValueDecoderImpl( + userInfo: self.impl.userInfo, + from: value, + codingPath: newPath, + options: self.impl.options + ) + } + + @inline(__always) private func getValue(forKey key: LocalKey) throws -> LDValue { + guard let value = dictionary[key.stringValue] else { + throw DecodingError.keyNotFound(key, .init( + codingPath: self.codingPath, + debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")." + )) + } + + return value + } + + @inline(__always) private func createTypeMismatchError(type: Any.Type, forKey key: K, value: LDValue) -> DecodingError { + let codingPath = self.codingPath + [key] + return DecodingError.typeMismatch(type, .init( + codingPath: codingPath, debugDescription: "Expected to decode \(type) but found \(value) instead." + )) + } + + @inline(__always) private func decodeFixedWidthInteger(key: Self.Key) throws -> T { + let value = try getValue(forKey: key) + return try self.impl.unwrapFixedWidthInteger(from: value, for: key, as: T.self) + } + + @inline(__always) private func decodeFloatingPoint(key: K) throws -> T { + let value = try getValue(forKey: key) + return try self.impl.unwrapFloatingPoint(from: value, for: key, as: T.self) + } + } +} + +extension LDValueDecoderImpl { + struct UnkeyedContainer: UnkeyedDecodingContainer { + let impl: LDValueDecoderImpl + let codingPath: [CodingKey] + let array: [LDValue] + + var count: Int? { self.array.count } + var isAtEnd: Bool { self.currentIndex >= (self.count ?? 0) } + var currentIndex = 0 + + init(impl: LDValueDecoderImpl, codingPath: [CodingKey], array: [LDValue]) { + self.impl = impl + self.codingPath = codingPath + self.array = array + } + + mutating func decodeNil() throws -> Bool { + if try self.getNextValue(ofType: Never.self) == .null { + self.currentIndex += 1 + return true + } + + // The protocol states: + // If the value is not null, does not increment currentIndex. + return false + } + + mutating func decode(_ type: Bool.Type) throws -> Bool { + let value = try self.getNextValue(ofType: Bool.self) + guard case .bool(let bool) = value else { + throw impl.createTypeMismatchError(type: type, for: LDValueKey(index: currentIndex), value: value) + } + + self.currentIndex += 1 + return bool + } + + mutating func decode(_ type: String.Type) throws -> String { + let value = try self.getNextValue(ofType: String.self) + guard case .string(let string) = value else { + throw impl.createTypeMismatchError(type: type, for: LDValueKey(index: currentIndex), value: value) + } + + self.currentIndex += 1 + return string + } + + mutating func decode(_: Double.Type) throws -> Double { + try decodeFloatingPoint() + } + + mutating func decode(_: Float.Type) throws -> Float { + try decodeFloatingPoint() + } + + mutating func decode(_: Int.Type) throws -> Int { + try decodeFixedWidthInteger() + } + + mutating func decode(_: Int8.Type) throws -> Int8 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: Int16.Type) throws -> Int16 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: Int32.Type) throws -> Int32 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: Int64.Type) throws -> Int64 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: UInt.Type) throws -> UInt { + try decodeFixedWidthInteger() + } + + mutating func decode(_: UInt8.Type) throws -> UInt8 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: UInt16.Type) throws -> UInt16 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: UInt32.Type) throws -> UInt32 { + try decodeFixedWidthInteger() + } + + mutating func decode(_: UInt64.Type) throws -> UInt64 { + try decodeFixedWidthInteger() + } + + mutating func decode(_ type: T.Type) throws -> T where T: Decodable { + let newDecoder = try decoderForNextElement(ofType: type) + let result = try newDecoder.unwrap(as: type) + + // Because of the requirement that the index not be incremented unless + // decoding the desired result type succeeds, it can not be a tail call. + // Hopefully the compiler still optimizes well enough that the result + // doesn't get copied around. + self.currentIndex += 1 + return result + } + + mutating func nestedContainer(keyedBy type: NestedKey.Type) throws + -> KeyedDecodingContainer where NestedKey: CodingKey + { + let decoder = try decoderForNextElement(ofType: KeyedDecodingContainer.self) + let container = try decoder.container(keyedBy: type) + + self.currentIndex += 1 + return container + } + + mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + let decoder = try decoderForNextElement(ofType: UnkeyedDecodingContainer.self) + let container = try decoder.unkeyedContainer() + + self.currentIndex += 1 + return container + } + + mutating func superDecoder() throws -> Decoder { + let decoder = try decoderForNextElement(ofType: Decoder.self) + self.currentIndex += 1 + return decoder + } + + private mutating func decoderForNextElement(ofType: T.Type) throws -> LDValueDecoderImpl { + let value = try self.getNextValue(ofType: T.self) + let newPath = self.codingPath + [LDValueKey(index: self.currentIndex)] + + return LDValueDecoderImpl( + userInfo: self.impl.userInfo, + from: value, + codingPath: newPath, + options: self.impl.options + ) + } + + @inline(__always) + private func getNextValue(ofType: T.Type) throws -> LDValue { + guard !self.isAtEnd else { + var message = "Unkeyed container is at end." + if T.self == UnkeyedContainer.self { + message = "Cannot get nested unkeyed container -- unkeyed container is at end." + } + if T.self == Decoder.self { + message = "Cannot get superDecoder() -- unkeyed container is at end." + } + + var path = self.codingPath + path.append(LDValueKey(index: self.currentIndex)) + + throw DecodingError.valueNotFound( + T.self, + .init(codingPath: path, + debugDescription: message, + underlyingError: nil)) + } + return self.array[self.currentIndex] + } + + @inline(__always) private mutating func decodeFixedWidthInteger() throws -> T { + let value = try self.getNextValue(ofType: T.self) + let key = LDValueKey(index: self.currentIndex) + let result = try self.impl.unwrapFixedWidthInteger(from: value, for: key, as: T.self) + self.currentIndex += 1 + return result + } + + @inline(__always) private mutating func decodeFloatingPoint() throws -> T { + let value = try self.getNextValue(ofType: T.self) + let key = LDValueKey(index: self.currentIndex) + let result = try self.impl.unwrapFloatingPoint(from: value, for: key, as: T.self) + self.currentIndex += 1 + return result + } + } +} diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 7beaa874..63325eb8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -145,7 +145,7 @@ public struct ApplicationInfo: Equatable { } let sanitized = unwrapped.replacingOccurrences(of: " ", with: "-") - if let error = validate(sanitized) { + if validate(sanitized) != nil { return } diff --git a/LaunchDarkly/LaunchDarklyTests/LDValueDecoderSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDValueDecoderSpec.swift new file mode 100644 index 00000000..2a65532c --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/LDValueDecoderSpec.swift @@ -0,0 +1,142 @@ +@testable import LaunchDarkly +import Foundation +import XCTest + +final class LDValueDecoderSpec: XCTestCase { + func testDecodeBooleans() { + let isFalse: LDValue = false + let isTrue: LDValue = true + + XCTAssertEqual(self.decode(Bool.self, value: isFalse), false) + XCTAssertEqual(self.decode(Bool.self, value: isTrue), true) + } + + func testDecodeStrings() { + let name: LDValue = "First A. Last" + let empty: LDValue = "" + + XCTAssertEqual(self.decode(String.self, value: name), "First A. Last") + XCTAssertEqual(self.decode(String.self, value: empty), "") + } + + func testDecodeNumbers() { + let theLoneliestNumber: LDValue = 1 + let pi: LDValue = 3.14 + + XCTAssertEqual(self.decode(Int.self, value: theLoneliestNumber), 1) + XCTAssertEqual(self.decode(Int.self, value: pi), 3) + + XCTAssertEqual(self.decode(Double.self, value: theLoneliestNumber), 1.0) + XCTAssertEqual(self.decode(Double.self, value: pi), 3.14) + } + + func testDecodeArrays() { + let fruit: LDValue = ["Apple", "Banana", "Cucumber"] + XCTAssertEqual(self.decode([String].self, value: fruit), ["Apple", "Banana", "Cucumber"]) + } + + func testDecodeDictionaries() { + let address: LDValue = ["street": "123 Easy St", "city": "Anytown", "state": "CA"] + let stats: LDValue = ["children": 3, "age": 79] + + XCTAssertEqual(self.decode([String: String].self, value: address), ["street": "123 Easy St", "city": "Anytown", "state": "CA"]) + XCTAssertEqual(self.decode([String: Int].self, value: stats), ["children": 3, "age": 79]) + } + + struct SimpleDecodable: Decodable { + public let firstName: String + public let age: Int + public let address: [String: String] + } + + func testDecodeDecodableType() { + let simpleLDValue = LDValue(dictionaryLiteral: ("firstName", "Danny"), ("age", 79), ("address", ["street": "123 Easy St", "city": "Anytown", "state": "CA"])) + + let decoded = self.decode(SimpleDecodable.self, value: simpleLDValue) + + XCTAssertEqual(decoded?.firstName, "Danny") + XCTAssertEqual(decoded?.age, 79) + XCTAssertEqual(decoded?.address["street"], "123 Easy St") + XCTAssertEqual(decoded?.address["city"], "Anytown") + XCTAssertEqual(decoded?.address["state"], "CA") + } + + struct ComplexDecodable: Decodable { + public let firstName: String + public let lastName: String + public let address: Address + + private struct DynamicCodingKeys: CodingKey { + // Protocol required implementations + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + return nil + } + + // Convenience method since we don't want to unwrap everywhere + init(string: String) { + self.stringValue = string + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + guard case .some(let bio) = try container.decodeIfPresent([String: String].self, forKey: DynamicCodingKeys(string: "bio")) else { + throw DecodingError.valueNotFound([String: String].self, DecodingError.Context(codingPath: [DynamicCodingKeys(string: "bio")], debugDescription: "bio must be present and a dictionary of strings")) + } + + guard let firstName = bio["firstName"] else { + throw DecodingError.valueNotFound(String.self, DecodingError.Context(codingPath: [DynamicCodingKeys(string: "bio"), DynamicCodingKeys(string: "firstName")], debugDescription: "bio must contain first name")) + } + + self.firstName = firstName + + guard let lastName = bio["lastName"] else { + throw DecodingError.valueNotFound(String.self, DecodingError.Context(codingPath: [DynamicCodingKeys(string: "bio"), DynamicCodingKeys(string: "lastName")], debugDescription: "bio must contain last name")) + } + + self.lastName = lastName + + guard case .some(let address) = try container.decodeIfPresent(Address.self, forKey: DynamicCodingKeys(string: "addy")) else { + throw DecodingError.valueNotFound(String.self, DecodingError.Context(codingPath: [DynamicCodingKeys(string: "addy")], debugDescription: "addy must contain address information")) + } + + self.address = address + } + } + + struct Address: Decodable { + public let street: String + public let city: String + public let state: String + } + + func testCustomDecodableType() { + let bio: LDValue = ["firstName": "Danny", "lastName": "DeVito"] + let address: LDValue = ["street": "123 Easy St", "city": "Anytown", "state": "CA"] + + let user: LDValue = ["bio": bio, "addy": address] + let decoded = self.decode(ComplexDecodable.self, value: user) + + XCTAssertEqual(decoded?.firstName, "Danny") + XCTAssertEqual(decoded?.lastName, "DeVito") + XCTAssertEqual(decoded?.address.street, "123 Easy St") + XCTAssertEqual(decoded?.address.city, "Anytown") + XCTAssertEqual(decoded?.address.state, "CA") + } + + private func decode(_ type: T.Type, value: LDValue) -> T? where T: Decodable { + do { + return try LDValueDecoder().decode(type, from: value) + } catch { + return nil + } + } +}