はじめに
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ジェネレータでメタまみれになってみませんか?