Generator の歩き方
趣旨
script/generate はいわずと知れた Rails のコードジェネレータである。
% cd $RAILSAPP % ruby script/generate scaffold MyModel
などとして使う。このコードでは MyModel に関するコードの雛形が生成されるわけだ。
今日は、この script/generate がどのように動作するか、しつこく追いかけてみる。
(コードジェネレータの内部構造については Generatorプラグインの作り方 に簡潔に書かれている。とてもおすすめ。瀧口さん、いつもお世話になります(ペコリ))
コマンドが実行されるまで
まずは、$RAILSAPP/script/generate の中身をのぞいてみよう。*1
#!/usr/bin/env ruby require File.dirname(__FILE__) + '/../config/boot' # (A) require 'commands/generate' # (B) #($RAILSAPP/script/generate)
(A) は Rails フレームワークの読み込みである。ここでロードパスが設定される。そのため (B) では、$GEMSHOME/rails-1.2.3/lib/commands/generate が読み込まれる。*2
では次にその中身を見てみよう。
require "#{RAILS_ROOT}/config/environment" require 'rails_generator' require 'rails_generator/scripts/generate' ARGV.shift if ['--help', '-h'].include?(ARGV[0]) Rails::Generator::Scripts::Generate.new.run(ARGV) #($GEMSHOME/rails-1.2.3/lib/commands/generate.rb)
さて Rails::Generator::Scripts::Generate というクラスはどこで定義されているのか。これはクラス名からも察しが付くように、rails_generator/scripts/generate.rb にあった。
require File.dirname(__FILE__) + '/../scripts' module Rails::Generator::Scripts class Generate < Base mandatory_options :command => :create # (X) => 後述 end end #($GEMSHOME/rails-1.2.3/lib/rails_generator/scripts/generate.rb)
たったこれだけある。Rails::Generator::Scripts::Generate クラスの実体はスーパークラスの Rails::Generator::Scripts::Base にある。
Base#run を見てみようか。
def run(args = [], runtime_options = {}) # (オプション解析) Rails::Generator::Base.instance(options[:generator], args, options).command(options[:command]).invoke! # (例外処理) end #($GEMSHOME/rails-1.2.3/lib/rails_generator/scripts.rb)
と大胆にはしょってしまおう。実質上の1行だけである。
Rails::Generator::Base.instance() は lookup.rb に定義がある。見ての通り、options[:generator] という名前のジェネレータのインスタンスを探している。そして、command() で options[:command] という名前のコマンドを見つけ出し、invoke! で実行。options[:command] はデフォルトで :create になっており(上記(X)参照) create.rb にある Rails::Generator::Commands::Create が生成される。つまりデフォルトだと何らかのコードが生成される。(って当たり前か)
何が実行されるのか?
では上で (Create クラスインスタンス).invoke! で何が実行されるのか? 答えはスーパークラスの Rails::Generator::Commands::Base#invoke! にある。
def invoke! manifest.replay(self) end #($GEMSHOME/rails-1.2.3/lib/rails_generator/commands.rb)
単純だ。manifest() して replay() しているだけだ。 この二つをそれぞれ見ていく。
まずは manifest()
manifest() の方だが、Rails::Generator::Commands::Base の delegate(たらいまわし先)である Rails::Generator::Base で定義されている。(Rails::Generator::Commands::Base の定義を見ると class Base < DelegateClass(Rails::Generator::Base) と書いてある。DelegateClass() については、文末の補足説明を参照のこと)
Rails::Generator::Base#manifest() の定義は空であり、サブクラスで定義をするようになっている。
def manifest raise NotImplementedError, "No manifest for '#{spec.name}' generator." end #($GEMSHOME/rails-1.2.3/lib/rails_generator/base.rb)
実際には、Rails::Generator::Base から派生した Rails::Generator::NamedBase のサブクラスのなかで manifest() を定義するようだ。たとえば次のように。
class ControllerGenerator < Rails::Generator::NamedBase def manifest record do |m| # Check for class naming collisions. m.class_collisions class_path, "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper" # Controller, helper, views, and test directories. m.directory File.join('app/controllers', class_path) m.directory File.join('app/helpers', class_path) m.directory File.join('app/views', class_path, file_name) m.directory File.join('test/functional', class_path) # Controller class, functional test, and helper class. m.template 'controller.rb', File.join('app/controllers', class_path, "#{file_name}_controller.rb") m.template 'functional_test.rb', File.join('test/functional', class_path, "#{file_name}_controller_test.rb") m.template 'helper.rb', File.join('app/helpers', class_path, "#{file_name}_helper.rb") # View template for each action. actions.each do |action| path = File.join('app/views', class_path, file_name, "#{action}.rhtml") m.template 'view.rhtml', path, :assigns => { :action => action, :path => path } end end end end #($GEMSHOME/rails-1.2.3/lib/rails_generator/generators/components/controller/controller_generator.rb)
これは % script/generate controller MyController ... とかして使うコントローラのジェネレータ本体である。record() は Rails::Generator::Base で定義されている。
def record Rails::Generator::Manifest.new(self) { |m| yield m } end #($GEMSHOME/rails-1.2.3/lib/rails_generator/base.rb)
ここで真打 Rails::Generator::Manifest の登場となる。このクラスの定義で、関係する部分を掲載する。
class Manifest attr_reader :target # Take a default action target. Yield self if block given. def initialize(target = nil) @target, @actions = target, [] yield self if block_given? end # Record an action. def method_missing(action, *args, &block) @actions << [action, args, block] end # Replay recorded actions. def replay(target = nil) send_actions(target || @target, @actions) end #(中略) private def send_actions(target, actions) actions.each do |method, args, block| target.send(method, *args, &block) end end end #($GEMSHOME/rails-1.2.3/lib/rails_generator/manifest.rb)
Rails::Generator::Manifest#initialize を見ればわかるが、要は、record() は Manifest インスタンスを生成して、ブロックに渡しているだけ。そのブロックの中で、Manifest インスタンスに
m.directory File.join('app/controllers', class_path) m.template 'controller.rb', File.join('app/controllers', class_path, "#{file_name}_controller.rb")
等々実行させたいアクションを登録していくわけだ。アクションを登録するやり方が面白い。method_missing() を使って一行で処理している。Ruby の面目躍如だ。
ここまでで manifest() の解説終わり。(ながっ)
directory() や template() は何を実行しているの?
Rails::Generator::Commands::Create を見てみよう。たとえば template の定義は次のとおりだ。
def template(relative_source, relative_destination, template_options = {}) file(relative_source, relative_destination, template_options) do |file| # Evaluate any assignments in a temporary, throwaway binding. vars = template_options[:assigns] || {} b = binding vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b } # Render the source file with the temporary binding. ERB.new(file.read, nil, '-').result(b) end end #($GEMSHOME/rails-1.2.3/lib/rails_generator/commands.rb)
基本的に file() というメソッドを呼び出している。おそらくはブロックを評価した結果を使ってファイルを生成するメソッドであろう。(確認したら実際そうだった。各自ソースコードを参照されたい)ここではブロックの中でテンプレートファイルを .rhtml ファイルのように埋め込まれた Ruby コードを評価した結果を返している。
directory() に関しても同様に実務的な処理を行っている。まさにここがジェネレータの核心であるといえる。
class_path て何よ?
上のコントローラ・ジェネレータのマニフェストにもあったけれど、class_path というのをよく見かける。これは Rails::Generator::NamedBase のアトリビュートとして定義されている。だが、いったい何を意味するのか?答えは、Rails::Generator::Base#extract_modules にあった。ここで引数 name には script/generate controller XXX の XXX の部分が入る。
def extract_modules(name) modules = name.include?('/') ? name.split('/') : name.split('::') name = modules.pop path = modules.map { |m| m.underscore } file_path = (path + [name.underscore]).join('/') nesting = modules.map { |m| m.camelize }.join('::') [name, path, file_path, nesting, modules.size] end #($GEMSHOME/rails-1.2.3/lib/rails_generator/base.rb)
呼び出し元では上のメソッドの中の path にあたる部分を Rails::Generator::NamedBase の class_path 属性に代入している。name = MyController::MySubController などと指定すると、path = ["my_controller"] となる。 (MyController/MySubController と "/" 区切りでもよい) name = A::B::C の場合は path = ["a", "b"] になる。結局、class_path は対象とするクラスにおけるモジュールの包含関係を表したものである、といえる。
結語
非常に長くなってしまった。ここまで読んでいるあなたは、かなり偉い。Rails の generator を書いたのは、Jeremy Kemper というお人らしいが、彼はかなりデザインパタンにはこだわりがあるようで、さまざまなパタンを駆使して美しいクラス構成を取っている。そのため汎用性・拡張性に優れている。反面、処理が方々のクラスに分散しているので、コードを追っかけるのが一苦労だった。基本的には非常に整然としたコードなので勉強のため読んでみるのもいいかもしれない。
補足1: DelegateClass() について
DelegateClass() というメソッドは次のようなクラスを自動的に作ってくれるものだと考えればいい。
class MyArray def initialize(delegate_array) @delegate_array = delegate_array end def push(x) @delegate_array.push(x) end def size @delegate_array.size end .... end ma = MyArray.new([]) ma.push(10) ma.push(20) ma.size # => 2
MyArray はコンストラクタで配列のインスタンスを取り、メソッド呼び出しを配列のインスタンスに転送するクラスである。普通に delegate をしようとすると、上のようにいちいち転送先のメソッドを転送元で定義してあげなければならないが、DelegateClass() を使うと次のように簡潔に書ける。
require 'delegate' class MyArray < DelegateClass(Array) end
補足2: デバック Tips
Generator 系のクラスでは、例外が途中で補足されてしまう。
raise my_interesting_variable
などとして状態を見ようとしても何も出力されない。
そういうときは、
% ruby script/generate foo_generator abc --backtrace
と --backtrace オプションをつけることで、バックトレースが表示されるようになる。