Swift4のCodableに対応した、独自のDecoder(CSVDecoder)を実装してみよう

# はじめに

Swift4で追加された `Codable` 。
実はエンコード・デコードできるのはJSON相手だけではありません。
Foundationには[PropertyListEncoder](https://developer.apple.com/documentation/foundation/propertylistencoder)、[PropertyListDecoder](https://developer.apple.com/documentation/foundation/propertylistdecoder)も存在します。
また、 `Decoder` `Encoder` プロトコルに適合した独自型を実装すれば、Codableの恩恵により、任意のデータ変換処理を型安全に取り扱うことができます。

というわけで今回は、 `Decoder` プロトコルの挙動理解を兼ねて、CSV(カンマ区切りテキスト)のデコーダを実装してみました。
既存の `JSONEncoder/Decoder` `PlistEncoder/Decoder` のコードを参考にしつつ、CSVの処理へと落とし込んでいます。
記述を単純化するため、拡張機能や例外処理は省略しています。**「カンマを含む文字列を表現できない」「要素が足りない列があるとクラッシュする」等の問題もあり、このまま実用できるものではありません。**
が、そのぶんDecoder周りの構造が見えやすいサンプルになっているかなと。

参考:
[swift/JSONEncoder.swift at master · apple/swift](https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift)
[swift/PlistEncoder.swift at master · apple/swift](https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/PlistEncoder.swift)

# 実装詳細

## ゴール

```csv
name,age,isMan
ほげ,25,true
ふが,100,false
```

上記のような、1行目がタイトル、2行目以降がデータになっているCSVデータを、

```swift
struct Row: Codable {
    let name: String
    let age: Int
    let isMan: Bool
}
```

このようなstructのArray `[Row]` へと変換する。

## コード

### 本体

```CSVDecoder.swift
import Foundation

//===----------------------------------------------------------------------===//
// CSV Decoder
//===----------------------------------------------------------------------===//

/// `CSVDecoder` facilitates the decoding of CSV into semantic `Decodable` types.
/// structでなくclassなのは、JSONDecoderやPlistDecoderの場合にはoptionを適宜切り替えつつdecodeしていけるようにだと思う
/// 実際の Decoder プロトコルへの適合は、fileprivateな _CSVRowDecoder 型を通して行う。
open class CSVDecoder {
    // MARK: - Constructing a CSV Decoder

    public init() {}

    open func decode<T : Decodable>(_ type: T.Type, from csv: String) throws -> [T] {
        var rows = csv.components(separatedBy: .newlines)
        let titleRow = rows.removeFirst()
        return try rows.map {
            let decoder = _CSVRowDecoder(titleRow: titleRow, valueRow: $0)
            return try T(from: decoder)
        }
    }
}

fileprivate class _CSVRowDecoder: Decoder {
    let titles: [String]
    let values: [String]

    var codingPath: [CodingKey?] { return [] }

    /// Contextual user-provided information for use during encoding.
    var userInfo: [CodingUserInfoKey : Any] { return [:] }

    // MARK: - Initialization

    /// Initializes `self` with the given top-level container and options.
    init(titleRow: String, valueRow: String) {
        titles = titleRow.split(separator: ",").map { String($0) }
        values = valueRow.split(separator: ",").map { String($0) }
    }

    // MARK: - Coding Path Operations

    /// T(from:)内で、各カラムのCodingPathをプロパティに接続するために呼び出されるのはこのメソッド
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
        let container = _CSVKeyedDecodingContainer<Key>(referencing: self)
        //注: 型消去
        return KeyedDecodingContainer(container)
    }

    func unkeyedContainer() throws -> UnkeyedDecodingContainer {
        //CSVだと関係ないけど、ネストを加味して考える場合、ここで対象がdictionayあるいはarrayだとか色々見てやる必要がある

        throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self,
                                          DecodingError.Context(codingPath: self.codingPath,
                                                                debugDescription: "Cannot get unkeyed decoding container -- found null value instead."))
    }

    func singleValueContainer() throws -> SingleValueDecodingContainer {
        throw DecodingError.typeMismatch(SingleValueDecodingContainer.self,
                                         DecodingError.Context(codingPath: self.codingPath,
                                                               debugDescription: "Cannot get single value decoding container -- found keyed container instead."))
    }
}

// MARK: Decoding Containers

fileprivate struct _CSVKeyedDecodingContainer<K : CodingKey> : KeyedDecodingContainerProtocol {
    typealias Key = K

    // MARK: Properties

    /// A reference to the decoder we're reading from.
    let decoder: _CSVRowDecoder

    /// Data we're reading from.
    let columns: [String : String]

    /// The path of coding keys taken to get to this point in decoding.
    var codingPath: [CodingKey?]

    // MARK: - Initialization

    /// Initializes `self` by referencing the given decoder.
    init(referencing decoder: _CSVRowDecoder) {
        self.decoder = decoder
        self.codingPath = decoder.codingPath
        columns = Dictionary(uniqueKeysWithValues: zip(decoder.titles, decoder.values))
    }

    // MARK: - KeyedDecodingContainerProtocol Methods

    var allKeys: [Key] {
        return columns.keys.flatMap { Key(stringValue: $0) }
    }

    func contains(_ key: Key) -> Bool {
        return columns[key.stringValue] != nil
    }

    func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
        // ここらへん、既存コードでは `unbox` というメソッドを通して具体処理を切り離してるんだけど、今回はダイレクトに書く
        // KeyedDecodingContainerProtocolとSingleValueDecodingContainerの具体処理を共通化したいとき、 `unbox` メソッドが効いてくるんだと思う
        return columns[key.stringValue].flatMap { Bool($0) }
    }

    func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? {
        return columns[key.stringValue].flatMap { Int($0) }
    }

    func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? {
        return columns[key.stringValue].flatMap { Int8($0) }
    }

    func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? {
        return columns[key.stringValue].flatMap { Int16($0) }
    }

    func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? {
        return columns[key.stringValue].flatMap { Int32($0) }
    }

    func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? {
        return columns[key.stringValue].flatMap { Int64($0) }
    }

    func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? {
        return columns[key.stringValue].flatMap { UInt($0) }
    }

    func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? {
        return columns[key.stringValue].flatMap { UInt8($0) }
    }

    func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? {
        return columns[key.stringValue].flatMap { UInt16($0) }
    }

    func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? {
        return columns[key.stringValue].flatMap { UInt32($0) }
    }

    func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? {
        return columns[key.stringValue].flatMap { UInt64($0) }
    }

    func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? {
        return columns[key.stringValue].flatMap { Float($0) }
    }

    func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? {
        return columns[key.stringValue].flatMap { Double($0) }
    }

    func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? {
        return columns[key.stringValue]
    }

    func decodeIfPresent(_ type: Data.Type, forKey key: Key) throws -> Data? {
        return columns[key.stringValue]?.data(using: .utf8)
    }

    func decodeIfPresent<T : Decodable>(_ type: T.Type, forKey key: Key) throws -> T? {
        // Date等のデコード方法(timeInterval, etc)を動的に指定するのもここらへん作り込む
        // cf. https://github.com/apple/swift/blob/dbe77601f348583eb892a5b9fff09327e23b00c2/stdlib/public/SDK/Foundation/JSONEncoder.swift#L30-L72

        // その他Decodableな型に対応するにはSingleValueDecodingContainerの実装が必要
        // cf. https://github.com/apple/swift/blob/dbe77601f348583eb892a5b9fff09327e23b00c2/stdlib/public/SDK/Foundation/JSONEncoder.swift#L1456
        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: codingPath,
                                  debugDescription: "SingleValueDecodingContainerはとりあえず置いとく")
        )
    }

    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> {
        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: codingPath,
                                  debugDescription: "CSVでnestは考えない")
        )
    }

    func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: codingPath,
                                  debugDescription: "CSVでnestは考えない")
        )
    }

    func superDecoder() throws -> Decoder {
        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: codingPath,
                                  debugDescription: "CSVでnestは考えない")
        )
    }

    func superDecoder(forKey key: K) throws -> Decoder {
        throw DecodingError.dataCorrupted(
            DecodingError.Context(codingPath: codingPath,
                                  debugDescription: "CSVでnestは考えない")
        )
    }
}
```

### usage

```swift
let csv = """
name,age,isMan
ほげ,25,true
ふが,100,false
"""

let decoder = CSVDecoder()
let rows = try! decoder.decode(Row.self, from: csv)
dump(rows)
```

## dump結果

```
▿ 2 elements
  ▿ CodableExample.Row
    - name: "ほげ"
    - age: 25
    - isMan: true
  ▿ CodableExample.Row
    - name: "ふが"
    - age: 100
    - isMan: false
```

# 所感

`JSONDecoder` は `Any` の闇に対応しなきゃいけない関係上、結構読みづらいですが、本質的に必要な処理はそこまで複雑ではないです。
一度こういう単純なフォーマットで実装してみると、Codableの挙動を理解しやすくなりますね。