はじめに
Swift4で追加された Codable
。
実はエンコード・デコードできるのはJSON相手だけではありません。
FoundationにはPropertyListEncoder、PropertyListDecoderも存在します。
また、 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]
へと変換する。
コード
本体
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
所感
JSONDecoder
は Any
の闇に対応しなきゃいけない関係上、結構読みづらいですが、本質的に必要な処理はそこまで複雑ではないです。
一度こういう単純なフォーマットで実装してみると、Codableの挙動を理解しやすくなりますね。