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

はじめに

Swift4で追加された Codable
実はエンコード・デコードできるのはJSON相手だけではありません。
FoundationにはPropertyListEncoderPropertyListDecoderも存在します。
また、 Decoder Encoder プロトコルに適合した独自型を実装すれば、Codableの恩恵により、任意のデータ変換処理を型安全に取り扱うことができます。

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

参考:
swift/JSONEncoder.swift at master · apple/swift
swift/PlistEncoder.swift at master · apple/swift

実装詳細

ゴール

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

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

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

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

所感

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


『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。