750 lines
26 KiB
Swift
750 lines
26 KiB
Swift
//
|
|
// CSVRowDecoder.swift
|
|
// CSV
|
|
//
|
|
// Created by Yasuhiro Hatta on 2018/11/17.
|
|
// Copyright © 2018 yaslab. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// `CSVRowDecoder` facilitates the decoding of CSV row into semantic `Decodable` types.
|
|
open class CSVRowDecoder {
|
|
|
|
/// The strategy to use for decoding `Bool` values.
|
|
public enum BoolDecodingStrategy {
|
|
/// Decode the `Bool` using default initializer.
|
|
case `default`
|
|
|
|
/// Decode the `Bool` as a custom value decoded by the given closure.
|
|
case custom((_ value: String) throws -> Bool)
|
|
}
|
|
|
|
/// The strategy to use for decoding `Date` values.
|
|
public enum DateDecodingStrategy {
|
|
/// Defer to `Date` for decoding. This is the default strategy.
|
|
case deferredToDate
|
|
|
|
/// Decode the `Date` as a UNIX timestamp from a JSON number.
|
|
case secondsSince1970
|
|
|
|
/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
|
|
case millisecondsSince1970
|
|
|
|
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
|
|
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
|
case iso8601
|
|
|
|
/// Decode the `Date` as a string parsed by the given formatter.
|
|
case formatted(DateFormatter)
|
|
|
|
/// Decode the `Date` as a custom value decoded by the given closure.
|
|
case custom((_ value: String) throws -> Date)
|
|
}
|
|
|
|
/// The strategy to use for decoding `Data` values.
|
|
public enum DataDecodingStrategy {
|
|
// TODO: Implement unkeyed decoding container.
|
|
// /// Defer to `Data` for decoding.
|
|
// case deferredToData
|
|
|
|
/// Decode the `Data` from a Base64-encoded string. This is the default strategy.
|
|
case base64
|
|
|
|
/// Decode the `Data` as a custom value decoded by the given closure.
|
|
case custom((_ value: String) throws -> Data)
|
|
}
|
|
|
|
/// The strategy to use for decoding `nil` values.
|
|
public enum NilDecodingStrategy {
|
|
case empty
|
|
case custom((_ value: String) -> Bool)
|
|
}
|
|
|
|
/// The strategy to use in decoding bools. Defaults to `.default`.
|
|
open var boolDecodingStrategy: BoolDecodingStrategy = .default
|
|
|
|
/// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
|
|
open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
|
|
|
|
/// The strategy to use in decoding binary data. Defaults to `.base64`.
|
|
open var dataDecodingStrategy: DataDecodingStrategy = .base64
|
|
|
|
/// The strategy to use in decoding nil data. Defaults to `.empty`.
|
|
open var nilDecodingStrategy: NilDecodingStrategy = .empty
|
|
|
|
/// Contextual user-provided information for use during decoding.
|
|
open var userInfo: [CodingUserInfoKey: Any] = [:]
|
|
|
|
/// Options set on the top-level encoder to pass down the decoding hierarchy.
|
|
fileprivate struct _Options {
|
|
let boolDecodingStrategy: BoolDecodingStrategy
|
|
let dateDecodingStrategy: DateDecodingStrategy
|
|
let dataDecodingStrategy: DataDecodingStrategy
|
|
let nilDecodingStrategy: NilDecodingStrategy
|
|
let userInfo: [CodingUserInfoKey: Any]
|
|
}
|
|
|
|
/// The options set on the top-level decoder.
|
|
fileprivate var options: _Options {
|
|
return _Options(boolDecodingStrategy: boolDecodingStrategy,
|
|
dateDecodingStrategy: dateDecodingStrategy,
|
|
dataDecodingStrategy: dataDecodingStrategy,
|
|
nilDecodingStrategy: nilDecodingStrategy,
|
|
userInfo: userInfo)
|
|
}
|
|
|
|
/// Initializes `self` with default strategies.
|
|
public init() {}
|
|
|
|
/// Decodes a top-level value of the given type from the given CSV row representation.
|
|
open func decode<T: Decodable>(_ type: T.Type, from reader: CSVReader) throws -> T {
|
|
let decoder = _CSVRowDecoder(referencing: reader, options: self.options)
|
|
return try type.init(from: decoder)
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate final class _CSVRowDecoder: Decoder {
|
|
|
|
fileprivate let reader: CSVReader
|
|
|
|
fileprivate let options: CSVRowDecoder._Options
|
|
|
|
public var codingPath: [CodingKey] = []
|
|
|
|
public var userInfo: [CodingUserInfoKey: Any] {
|
|
return self.options.userInfo
|
|
}
|
|
|
|
fileprivate init(referencing reader: CSVReader, options: CSVRowDecoder._Options) {
|
|
self.reader = reader
|
|
self.options = options
|
|
}
|
|
|
|
public func container<Key: CodingKey>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
|
|
let container = CSVKeyedDecodingContainer<Key>(referencing: self)
|
|
return KeyedDecodingContainer(container)
|
|
}
|
|
|
|
public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
|
throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self,
|
|
DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "Cannot get unkeyed decoding container -- found null value instead."))
|
|
}
|
|
|
|
public func singleValueContainer() throws -> SingleValueDecodingContainer {
|
|
return self
|
|
}
|
|
|
|
}
|
|
|
|
fileprivate final class CSVKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
|
|
|
|
typealias Key = K
|
|
|
|
private let decoder: _CSVRowDecoder
|
|
|
|
public var codingPath: [CodingKey] {
|
|
return self.decoder.codingPath
|
|
}
|
|
|
|
public var allKeys: [Key] {
|
|
guard let headerRow = self.decoder.reader.headerRow else { return [] }
|
|
return headerRow.compactMap { Key(stringValue: $0) }
|
|
}
|
|
|
|
fileprivate init(referencing decoder: _CSVRowDecoder) {
|
|
self.decoder = decoder
|
|
}
|
|
|
|
private func value(for key: Key) throws -> String {
|
|
guard self.contains(key) else {
|
|
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\")."))
|
|
}
|
|
|
|
if let index = key.intValue {
|
|
return self.decoder.reader.currentRow![index]
|
|
} else {
|
|
return self.decoder.reader[key.stringValue]!
|
|
}
|
|
}
|
|
|
|
private func _valueNotFound(_ type: Any.Type) -> DecodingError {
|
|
let description = "Expected \(type) value but found null instead."
|
|
return .valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: description))
|
|
}
|
|
|
|
public func contains(_ key: Key) -> Bool {
|
|
guard let row = self.decoder.reader.currentRow else { return false }
|
|
|
|
if let index = key.intValue {
|
|
return index < row.count
|
|
} else {
|
|
guard let headerRow = self.decoder.reader.headerRow else {
|
|
return false
|
|
}
|
|
return headerRow.contains(key.stringValue)
|
|
}
|
|
}
|
|
|
|
public func decodeNil(forKey key: Key) throws -> Bool {
|
|
switch decoder.options.nilDecodingStrategy {
|
|
case .empty:
|
|
return try self.value(for: key).isEmpty
|
|
case .custom(let customClosure):
|
|
return customClosure(try self.value(for: key))
|
|
}
|
|
}
|
|
|
|
public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Bool.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: String.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Double.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Float.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Int.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Int8.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Int16.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Int32.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: Int64.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: UInt.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: UInt8.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: UInt16.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: UInt32.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: UInt64.self) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
|
|
let value = try self.value(for: key)
|
|
|
|
self.decoder.codingPath.append(key)
|
|
defer { self.decoder.codingPath.removeLast() }
|
|
|
|
guard let result = try self.decoder.unbox(value, as: type) else {
|
|
throw _valueNotFound(type)
|
|
}
|
|
return result
|
|
}
|
|
|
|
public func nestedContainer<NestedKey: CodingKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> {
|
|
// Not supported
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "nestedContainer(...) CSV does not support nested values")
|
|
)
|
|
}
|
|
|
|
public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
|
// Not supported
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "nestedUnkeyedContainer(...) CSV does not support nested values")
|
|
)
|
|
}
|
|
|
|
public func superDecoder() throws -> Decoder {
|
|
// Not supported
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "CSV does not support nested values")
|
|
)
|
|
}
|
|
|
|
public func superDecoder(forKey key: Key) throws -> Decoder {
|
|
// Not supported
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "CSV does not support nested values")
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
extension _CSVRowDecoder: SingleValueDecodingContainer {
|
|
|
|
private var value: String {
|
|
let key = self.codingPath.last!
|
|
if let index = key.intValue {
|
|
return self.reader.currentRow![index]
|
|
} else {
|
|
return self.reader[key.stringValue]!
|
|
}
|
|
}
|
|
|
|
private func expectNonNull(_ type: Any.Type) throws {
|
|
guard !self.decodeNil() else {
|
|
let description = "Expected \(type) but found null value instead."
|
|
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: description))
|
|
}
|
|
}
|
|
|
|
public func decodeNil() -> Bool {
|
|
switch options.nilDecodingStrategy {
|
|
case .empty:
|
|
return self.value.isEmpty
|
|
case .custom(let customClosure):
|
|
return customClosure(self.value)
|
|
}
|
|
|
|
}
|
|
|
|
public func decode(_ type: Bool.Type) throws -> Bool {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Bool.self)!
|
|
}
|
|
|
|
public func decode(_ type: Int.Type) throws -> Int {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Int.self)!
|
|
}
|
|
|
|
public func decode(_ type: Int8.Type) throws -> Int8 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Int8.self)!
|
|
}
|
|
|
|
public func decode(_ type: Int16.Type) throws -> Int16 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Int16.self)!
|
|
}
|
|
|
|
public func decode(_ type: Int32.Type) throws -> Int32 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Int32.self)!
|
|
}
|
|
|
|
public func decode(_ type: Int64.Type) throws -> Int64 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Int64.self)!
|
|
}
|
|
|
|
public func decode(_ type: UInt.Type) throws -> UInt {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: UInt.self)!
|
|
}
|
|
|
|
public func decode(_ type: UInt8.Type) throws -> UInt8 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: UInt8.self)!
|
|
}
|
|
|
|
public func decode(_ type: UInt16.Type) throws -> UInt16 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: UInt16.self)!
|
|
}
|
|
|
|
public func decode(_ type: UInt32.Type) throws -> UInt32 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: UInt32.self)!
|
|
}
|
|
|
|
public func decode(_ type: UInt64.Type) throws -> UInt64 {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: UInt64.self)!
|
|
}
|
|
|
|
public func decode(_ type: Float.Type) throws -> Float {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Float.self)!
|
|
}
|
|
|
|
public func decode(_ type: Double.Type) throws -> Double {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: Double.self)!
|
|
}
|
|
|
|
public func decode(_ type: String.Type) throws -> String {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: String.self)!
|
|
}
|
|
|
|
public func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
|
try self.expectNonNull(type)
|
|
return try self.unbox(self.value, as: type)!
|
|
}
|
|
|
|
}
|
|
|
|
extension _CSVRowDecoder {
|
|
|
|
private func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: String) -> DecodingError {
|
|
let description = "Expected to decode \(expectation) but found \(reality) instead."
|
|
return .typeMismatch(expectation, DecodingError.Context(codingPath: path, debugDescription: description))
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Bool.Type) throws -> Bool? {
|
|
if value.isEmpty { return nil }
|
|
|
|
switch self.options.boolDecodingStrategy {
|
|
case .default:
|
|
guard let bool = Bool(value) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return bool
|
|
|
|
case .custom(let closure):
|
|
return try closure(value)
|
|
}
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Int.Type) throws -> Int? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let int = Int(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return int
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Int8.Type) throws -> Int8? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let int8 = Int8(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return int8
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Int16.Type) throws -> Int16? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let int16 = Int16(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return int16
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Int32.Type) throws -> Int32? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let int32 = Int32(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return int32
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Int64.Type) throws -> Int64? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let int64 = Int64(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return int64
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: UInt.Type) throws -> UInt? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let uint = UInt(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return uint
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: UInt8.Type) throws -> UInt8? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let uint8 = UInt8(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return uint8
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: UInt16.Type) throws -> UInt16? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let uint16 = UInt16(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return uint16
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: UInt32.Type) throws -> UInt32? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let uint32 = UInt32(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return uint32
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: UInt64.Type) throws -> UInt64? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let uint64 = UInt64(value, radix: 10) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return uint64
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Float.Type) throws -> Float? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let float = Float(value) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return float
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: Double.Type) throws -> Double? {
|
|
if value.isEmpty { return nil }
|
|
|
|
guard let double = Double(value) else {
|
|
throw self._typeMismatch(at: self.codingPath, expectation: type, reality: value)
|
|
}
|
|
return double
|
|
}
|
|
|
|
fileprivate func unbox(_ value: String, as type: String.Type) throws -> String? {
|
|
if value.isEmpty { return nil }
|
|
|
|
return value
|
|
}
|
|
|
|
private func unbox(_ value: String, as type: Date.Type) throws -> Date? {
|
|
if value.isEmpty { return nil }
|
|
|
|
switch self.options.dateDecodingStrategy {
|
|
case .deferredToDate:
|
|
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, *) {
|
|
guard let date = _iso8601Formatter.date(from: value) 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):
|
|
guard let date = formatter.date(from: value) 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):
|
|
return try closure(value)
|
|
}
|
|
}
|
|
|
|
private func unbox(_ value: String, as type: Data.Type) throws -> Data? {
|
|
if value.isEmpty { return nil }
|
|
|
|
switch self.options.dataDecodingStrategy {
|
|
// TODO: Implement unkeyed decoding container.
|
|
// case .deferredToData:
|
|
// return try Data(from: self)
|
|
|
|
case .base64:
|
|
guard let data = Data(base64Encoded: value) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Encountered Data is not valid Base64."))
|
|
}
|
|
return data
|
|
|
|
case .custom(let closure):
|
|
return try closure(value)
|
|
}
|
|
}
|
|
|
|
fileprivate func unbox<T: Decodable>(_ value: String, as type: T.Type) throws -> T? {
|
|
if value.isEmpty { return nil }
|
|
|
|
if type == Date.self {
|
|
guard let date = try self.unbox(value, as: Date.self) else { return nil }
|
|
return (date as! T)
|
|
} else if type == Data.self {
|
|
guard let data = try self.unbox(value, as: Data.self) else { return nil }
|
|
return (data as! T)
|
|
} else if type == URL.self {
|
|
guard let url = URL(string: value) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "Invalid URL string."))
|
|
}
|
|
return (url as! T)
|
|
} else if type == Decimal.self {
|
|
guard let decimal = Decimal(string: value) else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
|
|
debugDescription: "Invalid Decimal string."))
|
|
}
|
|
return (decimal as! T)
|
|
} else {
|
|
return try type.init(from: self)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// Shared ISO8601 Date Formatter
|
|
//===----------------------------------------------------------------------===//
|
|
// NOTE: This value is implicitly lazy and _must_ be lazy.
|
|
// We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has.
|
|
// ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS.
|
|
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
|
|
fileprivate var _iso8601Formatter: ISO8601DateFormatter = {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = .withInternetDateTime
|
|
return formatter
|
|
}()
|