Sử dụng Codable được support ở Swift 4 để viết thử một Decoder (CSVDecoder)

Mở đầu

Như chúng ta đã biết Codable đã được thêm vào ở Swift4. Thực tế thì việc Encode, Decode không phải chỉ JSON mới có thể làm được. ở Foundation cũng đã có PropertyListEndcoder , PropertyListDecoder. Ngoài ra, việc sử dụng một Protocol Decoder Encoder độc lập , với lợi ích mà Codable mang lại , chúng ta có thể tuỳ ý xử lý biến đổi dữ liệu một cách an toàn.

Vì vậy mà hôm nay để hiểu hơn về cơ chế hoạt động của Decoder, chúng ta hãy cùng thực hiện thử decode file CSV (có dấu phẩy phân biệt giữa các data) nhé. Cách làm của tôi đó là vừa tham khảo phần code JSONEncoder/Decoder PlistEncoder/Decoder đã có vừa viết dần dần code xử lý file CSV.

Để đơn giản hoá file code, những tính năng mở rộng hay những phần xử lý nằm ngoài ví dụ đều được giản lược đi. Vì vậy chúng ta có thể nhìn kết cấu phần Decoder một cách dễ dàng và đơn giản hơn.

Tham khảo swift/JSONEncoder.swift at master · apple/swift swift/PlistEncoder.swift at master · apple/swift

Chi tiết thực hiện

Goal

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

Dữ liệu file CSV giả sử được kết cấu theo kiểu như bên trên, dòng một là title , từ dòng 2 trở đi là dữ liệu. Dữ liệu đó mục tiêu sẽ được decode thành dạng một mảng các struct (Array [Row]) như sau:

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

Code

File code của tôi như sau:

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)

Kết quả hàm dump

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

Bình luận

JSONDecoder phải xử lý tất cả các trường hợp, quan hệ nên việc đọc code của nó khá là phực tạp rối rắm. Bản chất của việc xử lý những cái cần thiết chỉ như bên trên không phức tạp lắm phải không? Mỗi khi chúng ta làm thử một format đơn giản kiểu như thế này chúng ta có thể hiểu được cơ chế hoạt động của Codable hơn.

Tham khảo

Qiita

All Rights Reserved