diff --git a/CSV.xcodeproj/project.pbxproj b/CSV.xcodeproj/project.pbxproj index d6105aa..520af73 100755 --- a/CSV.xcodeproj/project.pbxproj +++ b/CSV.xcodeproj/project.pbxproj @@ -63,6 +63,9 @@ 0EDF8EE31DDB73530068056A /* ReadmeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDF8ECF1DDB73370068056A /* ReadmeTests.swift */; }; 0EDF8EE41DDB73530068056A /* TrimFieldsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDF8ED01DDB73370068056A /* TrimFieldsTests.swift */; }; 0EDF8EE51DDB73530068056A /* UnicodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EDF8ED11DDB73370068056A /* UnicodeTests.swift */; }; + 3C89219E21484154004AA78A /* CSVReader+DecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C89219D21484153004AA78A /* CSVReader+DecodableTests.swift */; }; + 3C89219F21484154004AA78A /* CSVReader+DecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C89219D21484153004AA78A /* CSVReader+DecodableTests.swift */; }; + 3C8921A021484154004AA78A /* CSVReader+DecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C89219D21484153004AA78A /* CSVReader+DecodableTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -114,6 +117,7 @@ 0EDF8ECF1DDB73370068056A /* ReadmeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadmeTests.swift; sourceTree = ""; }; 0EDF8ED01DDB73370068056A /* TrimFieldsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrimFieldsTests.swift; sourceTree = ""; }; 0EDF8ED11DDB73370068056A /* UnicodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnicodeTests.swift; sourceTree = ""; }; + 3C89219D21484153004AA78A /* CSVReader+DecodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CSVReader+DecodableTests.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -233,6 +237,7 @@ isa = PBXGroup; children = ( 0EDF8ECD1DDB73370068056A /* CSVTests.swift */, + 3C89219D21484153004AA78A /* CSVReader+DecodableTests.swift */, 0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */, 0EDF8ECE1DDB73370068056A /* LineBreakTests.swift */, 0EDF8ECF1DDB73370068056A /* ReadmeTests.swift */, @@ -560,6 +565,7 @@ buildActionMask = 2147483647; files = ( 0EDF8EDE1DDB73520068056A /* ReadmeTests.swift in Sources */, + 3C89219F21484154004AA78A /* CSVReader+DecodableTests.swift in Sources */, 0EDF8EDC1DDB73520068056A /* CSVTests.swift in Sources */, 0EDF8EDF1DDB73520068056A /* TrimFieldsTests.swift in Sources */, 0E7F657C1EF6437E00E1E1A0 /* Version1Tests.swift in Sources */, @@ -602,6 +608,7 @@ buildActionMask = 2147483647; files = ( 0EDF8ED91DDB73520068056A /* ReadmeTests.swift in Sources */, + 3C89219E21484154004AA78A /* CSVReader+DecodableTests.swift in Sources */, 0EDF8ED71DDB73520068056A /* CSVTests.swift in Sources */, 0EDF8EDA1DDB73520068056A /* TrimFieldsTests.swift in Sources */, 0E7F657B1EF6437E00E1E1A0 /* Version1Tests.swift in Sources */, @@ -630,6 +637,7 @@ buildActionMask = 2147483647; files = ( 0EDF8EE31DDB73530068056A /* ReadmeTests.swift in Sources */, + 3C8921A021484154004AA78A /* CSVReader+DecodableTests.swift in Sources */, 0EDF8EE11DDB73530068056A /* CSVTests.swift in Sources */, 0EDF8EE41DDB73530068056A /* TrimFieldsTests.swift in Sources */, 0E7F657D1EF6437E00E1E1A0 /* Version1Tests.swift in Sources */, diff --git a/CSV.xcodeproj/xcshareddata/xcschemes/CSV-iOS.xcscheme b/CSV.xcodeproj/xcshareddata/xcschemes/CSV-iOS.xcscheme index 03718b4..5c5db6e 100644 --- a/CSV.xcodeproj/xcshareddata/xcschemes/CSV-iOS.xcscheme +++ b/CSV.xcodeproj/xcshareddata/xcschemes/CSV-iOS.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + codeCoverageEnabled = "YES" shouldUseLaunchSchemeArgsEnv = "YES"> (keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + let container = CSVKeyedDecodingContainer(referencing: self) + return KeyedDecodingContainer(container) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: self.codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null value instead.")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + let key = self.codingPath[0].stringValue + guard let value = self.valuesByColumn[key] else { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: self.codingPath, + debugDescription: "Cannot get single value container, value for key \(key) not found.")) + } + return CSVSingleValueDecodingContainer(codingPath: self.codingPath, value: value) // TODO: test path assumption + } + } + + private class CSVKeyedDecodingContainer : KeyedDecodingContainerProtocol { + typealias Key = K + + let decoder: CSVRowDecoder + + var codingPath: [CodingKey] { + return self.decoder.codingPath + } + + var allKeys: [K] { + return self.decoder.valuesByColumn.keys.compactMap { K(stringValue: $0) } + } + + var valuesByColumn: [String: String] { + return self.decoder.valuesByColumn + } + + func valueFor(column: CodingKey) -> String? { + return self.valuesByColumn[column.stringValue] + } + + init(referencing decoder: CSVRowDecoder) { + self.decoder = decoder + } + + func contains(_ key: K) -> Bool { + return self.valueFor(column: key) != nil + } + + func decodeNil(forKey key: K) throws -> Bool { + guard let value = self.valueFor(column: key) else { + return true + } + + if value.count == 0 { + return true + } + + return false + } + + // TODO: support DecodingError.keyNotFound + // TODO: support DecodingError.valueNotFound + func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.singleValueContainer().decode(type) + } + + func decode(_ type: String.Type, forKey key: K) throws -> String { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ String($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Double.Type, forKey key: K) throws -> Double { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Double($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Float.Type, forKey key: K) throws -> Float { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Float($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Int.Type, forKey key: K) throws -> Int { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Int($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Int8($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Int16($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Int32($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + } + + func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ Int64($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ UInt($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ UInt8($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ UInt16($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ UInt32($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let result = self.valueFor(column: key).flatMap({ UInt64($0) }) else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + return result + + } + + func decode(_ type: T.Type, forKey key: K) throws -> T where T : Decodable { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let stringValue = self.valueFor(column: key) else { + throw DecodingError.valueNotFound(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...)")) + } + + if type == Date.self || type == NSDate.self { + guard let container = try self.decoder.singleValueContainer() as? CSVSingleValueDecodingContainer else { + throw DecodingError.typeMismatch(type, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.valueFor(column: key) ?? "nil")'")) + } + + return try container.decode(Date.self) as! T + } else { + return try type.init(from: self.decoder) + } + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: codingPath, + debugDescription: "nestedContainer(...) CSV does not support nested values") + ) + } + + func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: codingPath, + debugDescription: "nestedUnkeyedContainer(...) CSV does not support nested values") + ) + } + + func superDecoder() throws -> Decoder { + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: codingPath, + debugDescription: "CSV does not support nested values") + ) + } + + func superDecoder(forKey key: K) throws -> Decoder { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + throw DecodingError.dataCorrupted( + DecodingError.Context(codingPath: codingPath, + debugDescription: "CSV does not support nested values") + ) + } + } + + class CSVSingleValueDecodingContainer : SingleValueDecodingContainer { + var codingPath: [CodingKey] + + let value: Any + + init(codingPath: [CodingKey], value: Any) { + self.codingPath = codingPath + self.value = value + } + private func expectNonNull(_ type: T.Type) throws { + guard !self.decodeNil() else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected \(type) but found null value instead.")) + } + } + + public func decodeNil() -> Bool { + if self.value is NSNull { + return true + } + if let stringValue = self.value as? String, + stringValue.count == 0 { + return true + } + return false + } + + public func decode(_ expectedType: Bool.Type) throws -> Bool { + try expectNonNull(Bool.self) + + if let number = self.value as? NSNumber { + // TODO: Add a flag to coerce non-boolean numbers into Bools? + if number === kCFBooleanTrue as NSNumber { + return true + } else if number === kCFBooleanFalse as NSNumber { + return false + } + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let bool = value as? Bool { + return bool + */ + + } + + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.value)'")) + } + + public func decode(_ expectedType: Int.Type) throws -> Int { + try expectNonNull(Int.self) + + let attemptNumber: NSNumber? + + let formatter = NumberFormatter() + formatter.allowsFloats = false + if let number = self.value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { + attemptNumber = number + } else if let stringValue = self.value as? String, + let number = formatter.number(from: stringValue), number !== kCFBooleanTrue, number !== kCFBooleanFalse { + attemptNumber = number + } else { + attemptNumber = nil + } + + guard let number = attemptNumber else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: codingPath, debugDescription: "decode(...) value '\(self.value)'")) + } + + let int = number.intValue + guard NSNumber(value: int) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return int + } + + public func decode(_ expectedType: Int8.Type) throws -> Int8 { + try expectNonNull(Int8.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let int8 = number.int8Value + guard NSNumber(value: int8) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(expectedType).")) + } + + return int8 + } + + public func decode(_ expectedType: Int16.Type) throws -> Int16 { + try expectNonNull(Int16.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let int16 = number.int16Value + guard NSNumber(value: int16) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(expectedType).")) + } + + return int16 + } + + public func decode(_ expectedType: Int32.Type) throws -> Int32 { + try expectNonNull(Int32.self) + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let int32 = number.int32Value + guard NSNumber(value: int32) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(expectedType).")) + } + + return int32 + } + + public func decode(_ expectedType: Int64.Type) throws -> Int64 { + try expectNonNull(Int64.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let int64 = number.int64Value + guard NSNumber(value: int64) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return int64 + } + + public func decode(_ expectedType: UInt.Type) throws -> UInt { + try expectNonNull(UInt.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let uint = number.uintValue + guard NSNumber(value: uint) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return uint + } + + public func decode(_ expectedType: UInt8.Type) throws -> UInt8 { + try expectNonNull(UInt8.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(value)> does not fit in \(expectedType).")) + } + + let uint8 = number.uint8Value + guard NSNumber(value: uint8) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(expectedType).")) + } + + return uint8 + } + + public func decode(_ expectedType: UInt16.Type) throws -> UInt16 { + try expectNonNull(UInt16.self) + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let uint16 = number.uint16Value + guard NSNumber(value: uint16) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return uint16 + } + + public func decode(_ expectedType: UInt32.Type) throws -> UInt32 { + try expectNonNull(UInt32.self) + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let uint32 = number.uint32Value + guard NSNumber(value: uint32) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return uint32 + } + + public func decode(_ expectedType: UInt64.Type) throws -> UInt64 { + try expectNonNull(UInt64.self) + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + let uint64 = number.uint64Value + guard NSNumber(value: uint64) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number <\(number)> does not fit in \(expectedType).")) + } + + return uint64 + } + + public func decode(_ expectedType: Float.Type) throws -> Float { + try expectNonNull(Float.self) + if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { + // We are willing to return a Float by losing precision: + // * If the original value was integral, + // * and the integral value was > Float.greatestFiniteMagnitude, we will fail + // * and the integral value was <= Float.greatestFiniteMagnitude, we are willing to lose precision past 2^24 + // * If it was a Float, you will get back the precise value + // * If it was a Double or Decimal, you will get back the nearest approximation if it will fit + let double = number.doubleValue + guard abs(double) <= Double(Float.greatestFiniteMagnitude) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed CSV number \(number) does not fit in \(expectedType).")) + } + + return Float(double) + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let double = value as? Double { + if abs(double) <= Double(Float.max) { + return Float(double) + } + overflow = true + } else if let int = value as? Int { + if let float = Float(exactly: int) { + return float + } + overflow = true + */ + +// } else if let string = value as? String, +// case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy { +// if string == posInfString { +// return Float.infinity +// } else if string == negInfString { +// return -Float.infinity +// } else if string == nanString { +// return Float.nan +// } + } + + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + public func decode(_ expectedType: Double.Type) throws -> Double { + try expectNonNull(Double.self) + + if let number = self.value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { + // We are always willing to return the number as a Double: + // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double + // * If it was a Float or Double, you will get back the precise value + // * If it was Decimal, you will get back the nearest approximation + return number.doubleValue + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let double = value as? Double { + return double + } else if let int = value as? Int { + if let double = Double(exactly: int) { + return double + } + overflow = true + */ + +// } else if let string = value as? String, +// case .convertFromString(let posInfString, let negInfString, let nanString) = self.options.nonConformingFloatDecodingStrategy { +// if string == posInfString { +// return Double.infinity +// } else if string == negInfString { +// return -Double.infinity +// } else if string == nanString { +// return Double.nan +// } + } + + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + public func decode(_ expectedType: String.Type) throws -> String { + try expectNonNull(String.self) + guard let string = value as? String else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + return string + } + + public func decode(_ expectedType: Date.Type) throws -> Date { + try expectNonNull(String.self) + /* + switch self.options.dateDecodingStrategy { + case .deferredToDate: + self.storage.push(container: value) + defer { self.storage.popContainer() } + return try Date(from: self) + + case .secondsSince1970: + let double = try self.unbox(value, as: Double.self)! + return Date(timeIntervalSince1970: double) + + case .millisecondsSince1970: + let double = try self.unbox(value, as: Double.self)! + return Date(timeIntervalSince1970: double / 1000.0) + + case .iso8601: + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + let string = try self.unbox(value, as: String.self)! + guard let date = _iso8601Formatter.date(from: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted.")) + } + + return date + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + + case .formatted(let formatter): + let string = try self.unbox(value, as: String.self)! + guard let date = formatter.date(from: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Date string does not match format expected by formatter.")) + } + + return date + + case .custom(let closure): + self.storage.push(container: value) + defer { self.storage.popContainer() } + return try closure(self) + }*/ + + let formatter = CSVReader.dateFormatter + + guard let string = value as? String else { + throw DecodingError.typeMismatch(expectedType, + DecodingError.Context(codingPath: self.codingPath, debugDescription: "Value '\(self.value)' is of type \(type(of: self.value))")) + } + + guard let result = formatter.date(from: string) else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "decode(...) for type \(expectedType) with value '\(self.value)'") + } + return result + } + + public func decode(_ type: T.Type) throws -> T { + try expectNonNull(type) +// return try self.unbox(self.value, as: type)! + throw DecodingError.typeMismatch(T.self, + DecodingError.Context(codingPath: self.codingPath, + debugDescription: "decode(\(type))")) + } + } + + public func readRow() throws -> T? where T: Decodable { + guard let headerRow = self.headerRow else { + throw DecodingError.typeMismatch(T.self, + DecodingError.Context(codingPath: [], + debugDescription: "readRow(): Header row required to map to Decodable") + ) + } + + guard let valuesRow = self.readRow() else { + return nil + } + + let valuesForColumns = Dictionary(uniqueKeysWithValues: zip(headerRow, valuesRow)) + + let decoder = CSVRowDecoder(codingPath: [], valuesByColumn: valuesForColumns) + return try T(from: decoder) + } } diff --git a/Tests/CSVTests/CSVReader+DecodableTests.swift b/Tests/CSVTests/CSVReader+DecodableTests.swift new file mode 100644 index 0000000..8f42e95 --- /dev/null +++ b/Tests/CSVTests/CSVReader+DecodableTests.swift @@ -0,0 +1,208 @@ +// +// CSVReader+DecodableTests.swift +// CSV +// +// Created by Ian Grossberg on 9/11/18. +// Copyright © 2018 yaslab. All rights reserved. +// + +import XCTest +@testable import CSV + +class CSVReader_DecodableTests: XCTestCase { + static let allTests = [ + ("testNoHeader", testNoHeader), + ("testBasic", testBasic), + ("testTypeMismatch", testTypeMismatch), + ] + + enum Enum: String, Decodable { + case first + case second + } + struct SupportedDecodableExample: Decodable, Equatable { + let intKey: Int + let stringKey: String + let optionalStringKey: String? + let dateKey: Date + let enumKey: Enum + + static func ==(left: SupportedDecodableExample, right: SupportedDecodableExample) -> Bool { + let formatter = CSVReader.dateFormatter + return left.intKey == right.intKey && left.stringKey == right.stringKey && left.optionalStringKey == right.optionalStringKey + //&& left.dateKey.compare(right.dateKey) == ComparisonResult.orderedSame // TODO: find more accurate conversion method, cannot compare directly likely because we are losing precision when in csv + && formatter.string(from: left.dateKey) == formatter.string(from: right.dateKey) + && left.enumKey == right.enumKey + } + + static var examples: [SupportedDecodableExample] { + return [ + SupportedDecodableExample(intKey: 12345, stringKey: "stringValue", optionalStringKey: nil, dateKey: Date(), enumKey: .first), + SupportedDecodableExample(intKey: 54321, stringKey: "stringValue2", optionalStringKey: "withValue", dateKey: Date(timeInterval: 100, since: Date()), enumKey: .second) + ] + } + } + + func testNoHeader() { + let noHeaderConfig = CSVReader.Configuration(hasHeaderRow: false, + trimFields: false, + delimiter: ",", + whitespaces: .whitespaces) + let noHeaderIt = "あ,い1,\"う\",えお\n,,x,".unicodeScalars.makeIterator() + let noHeaderCSV = try! CSVReader(iterator: noHeaderIt, configuration: noHeaderConfig) + + do { + let _: SupportedDecodableExample? = try noHeaderCSV.readRow() + XCTFail("Expect DecodingError.typeMismatch Error thrown") + } catch { + } + } + + func testBasic() { + let headerConfig = CSVReader.Configuration(hasHeaderRow: true, + trimFields: false, + delimiter: ",", + whitespaces: .whitespaces) + let exampleRecords = SupportedDecodableExample.examples + let dateFormatter = CSVReader.dateFormatter + + let headerIt = """ + stringKey,optionalStringKey,intKey,ignored,dateKey,enumKey + \(exampleRecords[0].stringKey),,\(exampleRecords[0].intKey),,\"\(dateFormatter.string(from: exampleRecords[0].dateKey))\",\(exampleRecords[0].enumKey) + \(exampleRecords[1].stringKey),\(exampleRecords[1].optionalStringKey!),\(exampleRecords[1].intKey),,\"\(dateFormatter.string(from: exampleRecords[1].dateKey))\",\(exampleRecords[1].enumKey) + """.unicodeScalars.makeIterator() + let headerCSV = try! CSVReader(iterator: headerIt, configuration: headerConfig) + + var records = [SupportedDecodableExample]() + do { + while let record: SupportedDecodableExample = try headerCSV.readRow() { + records.append(record) + } + } catch { + XCTFail("readRow() threw error: \(error)") + } + XCTAssertEqual(records.count, 2) + XCTAssertEqual(records[0], exampleRecords[0]) + XCTAssertEqual(records[1], exampleRecords[1]) + } + + func testTypeMismatch() { + let headerConfig = CSVReader.Configuration(hasHeaderRow: true, + trimFields: false, + delimiter: ",", + whitespaces: .whitespaces) + let exampleRecords = SupportedDecodableExample.examples + + let invalidFieldTypeIt = """ + stringKey,optionalStringKey,intKey,ignored + \(exampleRecords[0].stringKey),,this is a string where we expect an Int, + \(exampleRecords[1].stringKey),\(exampleRecords[1].optionalStringKey!),\(exampleRecords[1].intKey), + """.unicodeScalars.makeIterator() + let invalidFieldTypeCSV = try! CSVReader(iterator: invalidFieldTypeIt, configuration: headerConfig) + + do { + while let _: SupportedDecodableExample = try invalidFieldTypeCSV.readRow() { + } + XCTFail("Expect DecodingError.typeMismatch Error thrown") + } catch { + guard let error = error as? DecodingError else { + XCTFail("Expect DecodingError Error thrown") + return + } + switch error { + case let .typeMismatch(_, context): + XCTAssertEqual(context.codingPath[0].stringValue, "intKey", "Type Mismatch Error on unexpected field") + break + default: + XCTFail("Expect Type Mismatch Error thrown") + return + } + } + } + + func testTypeInvalidDateFormat() { + let headerConfig = CSVReader.Configuration(hasHeaderRow: true, + trimFields: false, + delimiter: ",", + whitespaces: .whitespaces) + let invalidFieldTypeIt = """ + dateKey,stringKey,optionalStringKey,intKey,ignored + al;ksdjf;akjsdf,asldkj,,1234, + """.unicodeScalars.makeIterator() + let invalidFieldTypeCSV = try! CSVReader(iterator: invalidFieldTypeIt, configuration: headerConfig) + + do { + while let _: SupportedDecodableExample = try invalidFieldTypeCSV.readRow() { + } + XCTFail("Expect DecodingError.dataCorrupted Error thrown") + } catch { + guard let error = error as? DecodingError else { + XCTFail("Expect DecodingError Error thrown") + return + } + switch error { + case let .dataCorrupted(context): + XCTAssertEqual(context.codingPath[0].stringValue, "dateKey", "Type Mismatch Error on unexpected field") + break + default: + XCTFail("Expect DecodingError.dataCorrupted Error thrown, got \(error)") + return + } + } + } + + struct UnsupportedDecodableExample: Decodable, Equatable { + let enumKey: Enum + + static func ==(left: UnsupportedDecodableExample, right: UnsupportedDecodableExample) -> Bool { + return left.enumKey == right.enumKey + } + + static var examples: [UnsupportedDecodableExample] { + return [ + UnsupportedDecodableExample(enumKey: .first), + UnsupportedDecodableExample(enumKey: .second) + ] + } + } + + func testUnsupportedDecodableField() { + let headerConfig = CSVReader.Configuration(hasHeaderRow: true, + trimFields: false, + delimiter: ",", + whitespaces: .whitespaces) + let exampleRecords = UnsupportedDecodableExample.examples + + let headerIt = """ + enumKey,optionalStringKey,intKey,ignored,dateKey + \(exampleRecords[0].enumKey),"hiiiii",123445,, + \(exampleRecords[1].enumKey),,54231,, + \("third"),,54231,, + """.unicodeScalars.makeIterator() + let headerCSV = try! CSVReader(iterator: headerIt, configuration: headerConfig) + + var records = [UnsupportedDecodableExample]() + do { + while let record: UnsupportedDecodableExample = try headerCSV.readRow() { + records.append(record) + } + XCTFail("Expect Data Corrupted Error thrown") + } catch { + guard let decodingError = error as? DecodingError else { + XCTFail("Expect DecodingError Error thrown, instead we go \(error)") + return + } + switch decodingError { + case let .dataCorrupted(context): + guard context.codingPath[0].stringValue == "enumKey" else { + XCTFail("Data Corrupted Error on unexpected field") + return + } + break + default: + XCTFail("Expect Data Corrupted Error thrown, instead we got \(decodingError)") + return + } + } + } +} diff --git a/Tests/CSVTests/CSVTests.swift b/Tests/CSVTests/CSVTests.swift index a19e97a..c38eb36 100644 --- a/Tests/CSVTests/CSVTests.swift +++ b/Tests/CSVTests/CSVTests.swift @@ -231,5 +231,5 @@ class CSVTests: XCTestCase { // XCTAssertEqual(rows[1]["name"], "name2") // XCTAssertNil(rows[1]["yyy"]) // } - + }