Rails の GeneratorGenerator を使ってみる


## はじめに
Rails でモデルを作ったりコントローラを作ったりするとき、よく `rails generate model ...` とか `rails generate controller ...`といったコマンドを使うと思いますが、今回採り上げる `GeneratorGenerator` は、そういう Model ジェネレータや Controller ジェネレータのようなジェネレータを作るジェネレータです[^memo]。

[^memo]: もともとは Rails のジェネレータを支えている Thor について調べながら何か書いてみようと思っていたのですが、いきなり Thor を話題にするより、まずは Rails の話から入ったほうが、書くにしても勢いをつけられそうかなと思いました。続きが書けるかは見通しはないのですけれどもね。

## 簡単なサンプル
さて、さっそくコマンドを使ってみましょう。ここでは Hello というジェネレータを作成しようとしています。

```
$ bundle exec rails generate generator Hello
```

これで `lib/generators/hello` というディレクトリができ、そこに `hello_generator.rb` その他のファイルが生成されます。
これがジェネレータになるわけですが、もちろん `rails generate` コマンドは基本的にテンプレートを作成するだけですから、`hello_generator.rb` の中身は次のようにほぼ空っぽです。

```ruby
class HelloGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)
end
```

空のままではつまらないので、とりあえず曲がりなりにも動いたなと思えるようになるまでコードを書き足してみましょう。

```ruby
class HelloGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)

  def hello
    puts "You specified `#{self.name}' as name"
  end

  def goodbye
    puts 'Bye-bye'
  end

  protected

  def foo
    puts :foo
  end

  private

  def baa
    puts :baa
  end
end
```

単純に puts するばかりのメソッドを追加してみました。では実行してみましょう。`hello` ジェネレータを `World` という引数を伴って実行しています。

```
$ bundle exec rails generate hello World
You specified `World' as name
Bye-bye
```

文字列が出力されて終了しましたね。文字列の出力だけですから、ジェネレータとしての機能をまったく満たしていない物足りなさはありますが、とにかく動作したことは確認できたと思います。今はとりあえずこれでよしとしておきましょう。

### 動作についての簡単な説明
ジェネレータの動きについて、みておきましょう。動作原理ついては見たままともいえるので説明なしに推測可能かもしれませんが、解説してみます。
まずユーザが `HelloGenerator` に対して渡した引数 `World` の扱いについて着目してみます。

`Rails::Generators::NamedBase` を継承した `HelloGenerator` は、デフォルトで引数をひとつとります。プログラム内部でこの引数の名前は `name` として認識され、`self.name` などのゲッタでアクセスできるようになります。

ですので、`hello` メソッド内で `self.name` を評価することで `World` という出力を得ているわけです。

続いてメソッドがどのように実行されているかを見てみましょう。
これはこれこそ見たままという感じです。つまり、メソッドが記述された順に何も書かずとも順次実行されます。ただし、protected や private のメソッドはその限りではありません[^thorgroup]。

[^thorgroup]: このように public メソッドが自然に順次実行されるのは `Rails::Generators::NamedBase` が `Thor::Group` を継承しているからです。

## テンプレートを使ってファイルに出力
ジェネレータであるからには、やはり何かファイルに出力しておくべきでしょう。ということで、簡単ですがそんなサンプルを作ってみたいと思います。ここでは Amazon Web Services の CloudSearch をイメージして、あらたに `CloudSearchGenerator` を作ってみます。

```
$ bin/rails g generator CloudSearch
```

まずはテンプレートの作成です。`lib/generators/cloud_search/templates/index_fields.yml.erb` にこんな感じのものを書いてみましょう。

```yaml
domain_name: <%= name %>
index_field:
<% attributes.each do |attr| -%>
  - index_field_name: <%= attr.name %>
    index_field_type: <%= attr.type %>
    <%- if attr.type == :string -%>
    text_options:
      default_value: ""
      source_field: ""
      return_enabled: true
      sort_enabled: true
      highlight_enabled: true
      analysis_scheme: "Japanese"
    <%- end -%>
<% end -%>
```

`option` については `string` タイプのフィールドのことしか書いていませんが、サンプルなのでご勘弁を。

続いてジェネレータ本体をつくります。

```ruby
class CloudSearchGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('../templates', __FILE__)

  argument :attributes, type: :array, default: [], banner: 'field:type'

  def output
    base_path = File.join("cloud_search", class_path)
    empty_directory base_path

    @path = File.join(base_path, "#{file_name}.yml")
    template "index_fields.yml.erb", @path
  end
end
```

これで完成です。
ではさっそく実行してみましょう。ここでは `mydoc` というドメインを作成し、そこに `title` と `content` というフィールドを設定することイメージしています。

```
$ bin/rails g cloud_search mydoc title:string content:string
      create  cloud_search
      create  cloud_search/mydoc.yml
```

ファイルが `cloud_search/mydoc.yml` として作成されました！ 中身も期待どおりできているはずです。

### 動作についての簡単な説明
`templates` ディレクトリにあるテンプレートは `template` メソッドで適用できます。簡単にファイルを生成できますね。

なお、`Rails::Generators::NamedBase` を継承したジェネレータでは、引数 `attributes` は特殊な動作をします。
`attributes` のパースの仕様は、DB のフィールドを想定して設計されているので、CloudSearch 用にはそのままでは適用しにくいところがあります。
使う場合にはパースの仕様をカスタマイズする必要があるでしょうが、この記事はサンプルを目的にしているのでここで留めておこうと思います。

## 最後に
ここでは CloudSearch をイメージしたファイルの生成などをおこないましたが、ジェネレータは、そのほかにも、たとえば引数に基づいた設定ファイルの生成なども可能でしょう。
引数を渡すのはユーザだけではなく、たとえばデプロイツールにやらせることも可能かもしれず、環境ごとに設定ファイルを生成しわけたい場合にも利用できるかもしれません。
アイディア次第でさまざまシーンで活用できるのではないかな、と思います。

ジェネレータはもともと「コードを生成するコード」でありメタな存在といえますが、この論法でいくと、Generatorジェネレータは「コードを生成するコードを生成するコード」であり、メタメタです。
このメタっぷりは、魔術が飛び交う Rails の世界にふさわしい興味深いプログラムではないかと思います。
みなさんもGeneratorジェネレータでメタまみれになってみませんか？