はじめに
Rails でモデルを作ったりコントローラを作ったりするとき、よく rails generate model ... とか rails generate controller ...といったコマンドを使うと思いますが、今回採り上げる GeneratorGenerator は、そういう Model ジェネレータや Controller ジェネレータのようなジェネレータを作るジェネレータです1。
簡単なサンプル
さて、さっそくコマンドを使ってみましょう。ここでは Hello というジェネレータを作成しようとしています。
$ bundle exec rails generate generator Hello
これで lib/generators/hello というディレクトリができ、そこに hello_generator.rb その他のファイルが生成されます。
これがジェネレータになるわけですが、もちろん rails generate コマンドは基本的にテンプレートを作成するだけですから、hello_generator.rb の中身は次のようにほぼ空っぽです。
class HelloGenerator < Rails::Generators::NamedBase
source_root File.expand_path('../templates', __FILE__)
end
空のままではつまらないので、とりあえず曲がりなりにも動いたなと思えるようになるまでコードを書き足してみましょう。
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 のメソッドはその限りではありません2。
テンプレートを使ってファイルに出力
ジェネレータであるからには、やはり何かファイルに出力しておくべきでしょう。ということで、簡単ですがそんなサンプルを作ってみたいと思います。ここでは Amazon Web Services の CloudSearch をイメージして、あらたに CloudSearchGenerator を作ってみます。
$ bin/rails g generator CloudSearch
まずはテンプレートの作成です。lib/generators/cloud_search/templates/index_fields.yml.erb にこんな感じのものを書いてみましょう。
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 タイプのフィールドのことしか書いていませんが、サンプルなのでご勘弁を。
続いてジェネレータ本体をつくります。
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ジェネレータでメタまみれになってみませんか?