Sourceryでのボイラープレートコードの自動生成入門

kuro-46.hatenablog.com

上のブログのSourceryについての詳細を記述した記事です。

今回試したコードは以下のリポジトリに記載されていますので、興味があれば見て頂けますと幸いです。

github.com

やりたいこと

public class SampleView: UIView {
}

上記のようなクラスがあります。このクラスの内部に下記のようなコードを書くことを想定します。

public class SampleView: UIView {

    @IBOutlet private var titleLabel: UILabel!
    @IBOutlet private var subTitleLabel: UILabel!

    public var title: String? {
        get {
            titleLabel.text
        }
        set {
            titleLabel.text = newValue
            titleLabel.isHidden = newValue == nil 
        }
    }

    public var subTitle: String? {
        get {
            subTitleLabel.text
        }
        set {
            subTitleLabel.text = newValue
            subTitleLabel.isHidden = newValue == nil 
        }
    }
}

このときに、titleとsubTitleについて同じようなロジックのコードを繰り返し書いています。

このような繰り返しのコード(ボイラープレートコード)を毎回手で書くのは面倒なため、自動生成の手段を調べました。

以前から機会があればSourceryを使ってみようと思っていたため、自分でStencilのテンプレートを書いて実装してみました。

今回初めてSourceryとStencilを使用する際に、分からないことが多く時間がかかったため、SourceryとStencilについてメモを書いておきます。

Sourcery

Sourceryは、Swiftでメタプログラミングを行うためのツールです。

詳細はリポジトリを見ていただければと思います。今回はHomebrewでinstallして、CommandLine上で実行しました。

$brew install sourcery

Command Line Option

Sourceryには、多くのOptionがあります。これらのOptionファイルは .sourcery.yml に書くこともできますが、今回は手作業で実行したかったため、Command Line上でOptionを入力しています。

必要になったOptionについて説明を記載しておきます。

  • --sources: SourceファイルのPathを記載します。複数指定可能。
  • --templates - TemplateファイルのPathを記載します。複数指定可能。
  • --output: OutputファイルのPathを記載します。
  • --args: その他の入力となるパラメータを入れることができます。(--args arg1=value,arg2 のような形)。 Argsはテンプレート内でargument.nameのような形でアクセスできます。

これらのOptionを組み合わせて、下記のように書くことで、現在使用されているファイルに対してOutputすることも可能です。

$sourcery --sources SampleView.swift --templates Sample.stencil --output SampleView.swift --args title=String,subTitle=String

Inline code generation

上のbashコマンドを入力すると、コードをファイル内に挿入するのではなく、ファイル全体が書き換わってしまいます。

コードは基本的にファイル単位で生成されますが、下のようなコメントをStencilファイル内に入れることで、コードを行間に入れることができます。

// sourcery:inline:{{ type.name }}.TemplateName
// sourcery:end

SwiftファイルにもStencilに対応して下のようなコメントを書く必要があります。このコメントの間にCodeを入れることができます。

// sourcery:inline:SampleView.TemplateName
// sourcery:end

参考:github.com

テンプレート作成

上で少し言及していましたが、SourceryではTemplateファイルを利用してコードを生成します。

独自のテンプレートを作成するには、Stencilというテンプレート言語でコードを書く必要があります。

StencilのGithubはコチラです。内容は上のサイトとほぼ同じです。

TemplateファイルのHello World(とりあえず実行してみる)

Stencilファイルを作り、下のようなコードを書いて、Sourceryのコマンドで引数を渡してコードを実行してみます。

StencilでSourceryから受け取るパタメータの形式については、Sourecry Reference内のWriting TemplateおよびTypesを参考にしました。

{% for type in types.classes %}
// sourcery:inline:{{ type.name }}.TemplateName
{{ type }}
// sourcery:end
{% endfor %}

実行するコマンドは以下。

$sourcery --sources Samples/Sample1.swift --templates Stencils/Sample1.stencil --output Samples/Sample1.swift

下のようなClassに関するtypeの情報が出力されることが確認できます。

class Sample1View: UIView {
    // sourcery:inline:Sample1View.TemplateName
    Class: module = nil, typealiases = [:], isExtension = false, kind = class, accessLevel = internal, name = Sample1View, isGeneric = false, localName = Sample1View, variables = [], methods = [], subscripts = [], initializers = [], annotations = [:], staticVariables = [], staticMethods = [], classMethods = [], instanceVariables = [], instanceMethods = [], computedVariables = [], storedVariables = [], inheritedTypes = ["UIView"], containedTypes = [], parentName = nil, parentTypes = AnyIterator<Type>(_box: Swift._IteratorBox<Swift._ClosureBasedIterator<SourceryRuntime.Type>>), attributes = [:], kind = class, isFinal = false
    // sourcery:end
}

引数を渡す

今回行いたいことは、title, subTitleのような文字列に対してコードを自動生成することです。

そのためSourceryのoptionの1つであるargsを利用します。argsはStencil内でargumentとして使用することができます。

Stencilファイルは、勉強のためにあえて改行や空白を入れて書いてみます。

{% for type in types.classes %}
// sourcery:inline:{{ type.name }}.TemplateName
{{ argument }}

    {% for key, value in argument %}
    {{ key }}: {{value}}
    {% endfor %}
// sourcery:end
{% endfor %}

実行するbashファイルは以下。

$sourcery --sources Samples/Sample2.swift --templates Stencils/Sample2.stencil --output Samples/Sample2.swift --args title=String,subTitle=String

出力結果は以下のようになります。

class Sample2: UIView {
    
    // sourcery:inline:Sample2.TemplateName
    ["subTitle": String, "title": String]

        subTitle: String
        title: String
    // sourcery:end
}

argumentを受け取って表示できていることを確認できたと思います。

また、stencilファイルで改行や空白を入れたところにも、結果が反映されていることが分かったと思います。

if, for文などの構文を書く

Stencilではif, for文などの構文はTagsという概念で扱うことができるようです。

コチラに、全ての対応しているTagsが記載されています。

上記の構文を利用して、下記のようなStencilファイルを作成します。

{% for type in types.classes %}
// sourcery:inline:{{ type.name }}.TemplateName
{% for key, value in argument %}
{% if value == "String" %}
@IBOutlet private var {{ key }}Label: UILabel!
{% endif %}
{% endfor %}
{% for key, value in argument %}
{% if value == "String" %}

public var {{ key }}: String? {
    get {
        {{ key }}Label.text
    }
    set {
        {{ key }}Label.text = newValue
        {{ key }}Label.isHidden = newValue == nil 
    }
}
{% endif %}
{% endfor %}
// sourcery:end
{% endfor %}

bashファイルは下記。

$sourcery --sources Samples/Sample3.swift --templates Stencils/Sample3.stencil --output Samples/Sample3.swift --args title=String,subTitle=String

出力されるSwiftファイルは下記です。今回やりたかったことが実現できました。

class Sample3: UIView {

    // sourcery:inline:Sample3.TemplateName
    @IBOutlet private var subTitleLabel: UILabel!
    @IBOutlet private var titleLabel: UILabel!

    public var subTitle: String? {
        get {
            subTitleLabel.text
        }
        set {
            subTitleLabel.text = newValue
            subTitleLabel.isHidden = newValue == nil 
        }
    }

    public var title: String? {
        get {
            titleLabel.text
        }
        set {
            titleLabel.text = newValue
            titleLabel.isHidden = newValue == nil 
        }
    }
    // sourcery:end
}

最後に

Sourceryはコードを生成する強力なツールであり、Stencilテンプレートを使用することでコードを自動生成できることを理解できました。

繰り返しになりますが、上記のコードは以下のリポジトリに記載されています。

github.com

興味があれば、下のブログも併せて読んでいただけますと幸いです。

kuro-46.hatenablog.com