🍏
SourceryでDecodable準拠を雑に自動化する
モチベーション
- SwiftでJSONデータをstruct, classにマッピングするとき
Decodable
を使うと楽 - Int, Double, Stringなど、JSONとSwiftの型が一致するプロパティのみで構成されている場合は
struct Xxx: Decodable
のように記述するだけでok - しかし
URL
やDate
などSwift側にしか存在しない型にパースするためには独自のデコード処理を記述する必要がある - ボイラープレートなコードを繰り返し書かなければいけない、型の指定やOptionalの扱いなど実装ミスに気をつけなければいけない、という問題がある
- その解決方法の1つとして自動化を図る
ゴール
以下のコードを入力とすると
import Foundation
/// sourcery: DecodableTarget
struct Foo {
let int: Int
let optionalInt: Int?
let array: [Int]
let url: URL
let optionalUrl: URL?
}
このようなアウトプットが得られます。
※ formatterにかけた上なので実際にはネストの位置はズレる
import Foundation
extension Foo: Decodable {
enum CodingKeys: String, CodingKey {
case int
case optionalInt
case array
case url
case optionalUrl
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.int = try values.decode(Int.self, forKey: .int)
self.optionalInt = try values.decodeIfPresent(Int.self, forKey: .optionalInt)
self.array = try values.decode([Int].self, forKey: .array)
let decodedUrl = try values.decode(String.self, forKey: .url)
guard let convertedUrl = URL(string: decodedUrl) else {
throw Error
}
self.url = convertedUrl
let decodedOptionalUrl = try values.decodeIfPresent(String.self, forKey: .optionalUrl)
let convertedOptionalUrl = decodedOptionalUrl.flatMap {
URL(string: $0)
}
self.optionalUrl = convertedOptionalUrl
}
}
自動化する
ツールの導入
Sourcery
というツールを利用します。
Swiftのコードを解析、テンプレートにパースすることでSwiftのコードを自動生成するものです。
Homebrew, Mint, CocoaPods, バイナリでインストール可能です。
どの方法でも可能ですが、プロジェクトごとにツールのバージョン管理したいため以降はMintを利用する前提で記述します。
入力ファイルの用意
Decodableに準拠させたいファイルを Input/
に配置します。
自動生成の対象struct, classには /// sourcery: DecodableTarget
というアノテーションを付けます。
自動化が難しく自前でデコード処理を書かなければいけない場合は、アノテーションを付けないことで自動生成の対象から外すことができます。
サンプルとして以下のstructを用意しました。
Input/Foo.swift
import Foundation
/// sourcery: DecodableTarget
struct Foo {
let int: Int
let optionalInt: Int?
let array: [Int]
let url: URL
let optionalUrl: URL?
}
テンプレートファイルの用意
詳細はドキュメントを参照してください。
サンプルとして URL
のみ対応していますが、他の任意の型も必要に応じて追加したり、生成方法を変更することができます。
template.stencil
import Foundation
{% for type in types.structs|annotated: "DecodableTarget" %}
extension {{ type.name }}: Decodable {
enum CodingKeys: String, CodingKey {
{% for variable in type.variables %}
case {{ variable.name }}
{% endfor %}
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
{% for variable in type.variables %}
{% if variable.unwrappedTypeName == "URL" %}
{% if variable.isOptional %}
let decoded{{ variable.name|upperFirstLetter }} = try values.decodeIfPresent(String.self, forKey: .{{ variable.name }})
let converted{{ variable.name|upperFirstLetter }} = decoded{{ variable.name|upperFirstLetter }}.flatMap { URL(string: $0) }
{% else %}
let decoded{{ variable.name|upperFirstLetter }} = try values.decode(String.self, forKey: .{{ variable.name }})
guard let converted{{ variable.name|upperFirstLetter }} = URL(string: decoded{{ variable.name|upperFirstLetter }}) else {
throw Error // TODO: replace to some error
}
{% endif %}
self.{{ variable.name }} = converted{{ variable.name|upperFirstLetter }}
{% else %}
{% if variable.isOptional %}
self.{{ variable.name }} = try values.decodeIfPresent({{ variable.unwrappedTypeName }}.self, forKey: .{{ variable.name }})
{% else %}
self.{{ variable.name }} = try values.decode({{ variable.typeName }}.self, forKey: .{{ variable.name }})
{% endif %}
{% endif %}
{% endfor %}
}
}
{% endfor %}
実行
.sourcery.yml
に設定を記述することも可能ですが説明の簡略化のためコマンドラインオプションで記述します。
mint run sourcery \
--sources Input \
--templates template.stencil \
--output Output.swift
出力結果
--output
で指定したファイルに ゴール のコードが出力されます。
Discussion