Ruby on Rails4を読む
はじめに †
Rails4では以下のようにアプリケーションのベースとなるファイルを生成するようです。
C:\Sites>rails new foo
C:\Sites>cd foo
C:\Sites\foo>rails generate scaffold user name:string email:string
アプリケーション名やらなんやらはRails入れるときに参考にしたサイトのサンプルそのまんまです。深い意味はありません。
さて、以前に読んだときはファイル生成周りを読んでませんが興味がわかなかったのか読んで萌えポイントがなかったのか、ともかく今回は読みます。ちらっと見ただけで萌えポイントがあったので:-)
PATH上にあるrailsコマンド †
まずはエントリーポイント、railsコマンドを見ていきます。
1
2
3
4
5
6
7
8
|
-
!
| require 'rubygems'
version = ">= 0"
gem 'railties', version
load Gem.bin_path('railties', 'rails', version)
|
というわけでrailtiesのrailsに進みます。「.gitがあったら」のコードを除くと以下の一行です。
railties/lib/rails/cli.rb †
rails/cli.rb。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
|
!
-
|
|
|
|
!
| require 'rails/app_rails_loader'
Rails::AppRailsLoader.exec_app_rails
require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }
if ARGV.first == 'plugin'
ARGV.shift
require 'rails/commands/plugin'
else
require 'rails/commands/application'
end
|
妙なことが書いてありますがいったん無視して下の方に注目します。(気になる人のために書いておくと「If we are inside Rails application」というのは「Railsアプリケーションディレクトリ内でコマンドが実行されたら」ということです。アプリディレクトリを作るときは当然、アプリディレクトリ内にはいないので下のコードが実行されます)
railties/lib/rails/commands/application.rb †
rails/commands/application.rb。
1
2
3
4
5
6
7
|
| require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
(中略)
args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
Rails::Generators::AppGenerator.start args
|
ARGVScrubberはapp_generator.rbの後半に書かれています。いろいろややこしく書いてありますがやっていることは以下の2点です。
- 変なコマンドが指定されてたら--helpが指定されてたということにする
- railsrcを読み込んでオプションに加える
さて、AppGenerator.startですがこれを追っかけるのはなかなか面倒です。Railsのソース読むためにはこの追っかけ方法を会得する必要があります:-)
rails/generators/rails/app/app_generator.rb
1
2
3
4
5
|
-
-
-
| require 'rails/generators/app_base'
module Rails
module Generators
class AppGenerator < AppBase
|
rails/generators/app_base.rb
1
2
3
4
5
|
-
-
-
| require 'rails/generators'
module Rails
module Generators
class AppBase < Base
|
base.rbはrequireしていない。しかし、generators.rbで以下のように書かれている。
rails/generators.rb
1
2
3
| -
-
|
| module Rails
module Generators
autoload :Base, 'rails/generators/base'
|
rails/generators/base.rb
1
2
3
4
5
|
-
-
-
| require 'thor/group'
module Rails
module Generators
class Base < Thor::Group
|
というわけで、gemが変わってthorに移動します。
thor/group.rb
1
2
3
4
|
| require "thor/base"
class Thor::Group # rubocop:disable ClassLength
include Thor::Base
|
thor/base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
-
|
-
-
|
|
|
!
|
-
-
|
|
!
| class Thor
module Base
class << self
def included(base) base.extend ClassMethods
base.send :include, Invocation
base.send :include, Shell
end
module ClassMethods
def start(given_args = ARGV, config = {})
config[:shell] ||= Thor::Base.shell.new
dispatch(nil, given_args.dup, nil, config)
end
|
というわけでようやくstartはけ〜んです。
Thor::Group.dispatch (thor/lib/thor/group.rb) †
さてというわけでdispatchというなんかコマンド実行してそうな雰囲気のメソッドにたどり着きました。見出しにもう書いてますがBaseのdispatchは例外投げるだけで実際にはGroupに定義されているdispatchが呼ばれます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| -
-
-
-
|
|
!
|
|
|
|
|
|
|
-
|
|
|
!
!
| class Thor::Group class << self
def dispatch(command, given_args, given_opts, config) if Thor::HELP_MAPPINGS.include?(given_args.first)
help(config[:shell])
return
end
args, opts = Thor::Options.split(given_args)
opts = given_opts || opts
instance = new(args, opts, config)
yield instance if block_given?
if command
instance.invoke_command(all_commands[command])
else
instance.invoke_all
end
end
|
オプションは付けてないので飛ばします。大したことはやってないので興味があったら見てみてください。
で、newされてます。やや見慣れない感じですが、selfはクラスオブジェクトなので通常通りインスタンス生成が行われます。クラスオブジェクトはAppGeneratorなのでrailtiesの方に戻りましょう。
railties/lib/rails/generators/rails/app/app_generator.rb
1
2
3
4
5
6
7
8
9
10
11
12
| -
-
|
|
-
|
!
|
-
|
!
!
| class AppGenerator < AppBase def initialize(*args)
super
unless app_path
raise Error, "Application name should be provided in arguments. For details run: rails --help"
end
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
end
end
|
「app_pathってなに〜!?」って心の叫びを今から解決します。superしてるのでスーパークラスのinitialize呼ばれます。実際にはAppBaseのinitializeが呼ばれてさらにそこからsuperされてますが飛ばしてThor::Baseのinitializeに移ります。
thor/lib/thor/base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
-
!
-
|
|
|
|
!
-
|
-
|
|
!
| def initialize(args = [], local_options = {}, config = {})
to_parse = args
to_parse += opts.remaining unless self.class.strict_args_position?(config)
thor_args = Thor::Arguments.new(self.class.arguments)
thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) }
@args = thor_args.remaining
end
|
また謎来ました。見た感じ、渡された引数を定義されている引数リスト(arguments)に基づいてsetter呼び出してる雰囲気です。とするとapp_pathという引数(argument)を定義しておけば先ほどのAppGenerator#initializeは問題がなくなりそうです。
ところで・・・、新しくアプリケーション作るときって、「rails new foo」でしたよね。ところがぎっちょん、今のargsには['foo']と'new'は除けられた配列になっています。いつの間にそうなったのかというと先ほどのARGVScrubber、
railties/lib/rails/generators/rails/app/app_generator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| -
|
-
|
!
!
-
-
|
|
|
!
!
-
-
|
|
|
!
!
-
-
|
|
|
-
!
!
|
-
|
|
|
!
| def prepare!
handle_version_request!(@argv.first)
handle_invalid_command!(@argv.first, @argv) do
handle_rails_rc!(@argv.drop(1))
end
end
def handle_invalid_command!(argument, argv)
if argument == "new"
yield
else
['--help'] + argv.drop(1)
end
end
def handle_rails_rc!(argv)
if argv.find { |arg| arg == '--no-rc' }
argv.reject { |arg| arg == '--no-rc' }
else
railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
end
end
def railsrc(argv)
if (customrc = argv.index{ |x| x.include?("--rc=") })
fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
else
yield argv, self.class.default_rc_file
end
end
def insert_railsrc_into_argv!(argv, railsrc)
return argv unless File.exist?(railsrc)
extra_args = read_rc_file railsrc
argv.take(1) + extra_args + argv.drop(1)
end
|
渡された引数配列をdrop(1)しているのでしれっとnewが除けられています。はじめ見たときは気づきませんでした。(argumentsまで来たところで、「あれ?newどこ行ったん?」と思いました)
話を戻してargumentです。AppGeneratorにはapp_pathの定義はありませんがスーパークラスのAppBaseにありました。
railties/lib/rails/generators/app_base.rb
1
2
| -
|
| class AppBase < Base argument :app_path, type: :string
|
argumentメソッドを貼るのは省略します。attr_accessorでsetter/getter作ってるのとargumentsにThor::Argumentインスタンスを追加しています。
Thor::Invocation::invoke_all (thor/lib/thor/invocation.rb) †
さて。ここまででやっとインスタンス生成が終わりました。ようやくコマンド実行です。invoke_allが呼ばれます。group.rbやbase.rbにinvoke_allない、と思ったら同ディレクトリにinvocation.rbがあってそこにありました。ちなみに、InvocationはBaseのincludedメソッドでincludeされてます・・・
1
2
3
4
5
| -
-
-
-
!
| class Thor
module Invocation
def invoke_all self.class.all_commands.map { |_, command| invoke_command(command) }
end
|
またまた謎のall_commandsが登場です。こちらはbase.rbに書かれています。
thor/lib/thor.base.rb
1
2
3
4
5
6
7
8
| -
|
|
!
-
|
!
| def all_commands
@all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
@all_commands.merge(commands)
end
def commands
@commands ||= Thor::CoreExt::OrderedHash.new
end
|
まあ想像通りかなと思います。OrderedHashってのはまあOrderedHashなのでしょう。
で・・・、commandsはいつ設定されるの?と先ほどのargument的なのを小一時間探したのですが見当たりませんでした。app_generator.rbを見ると
railties/lib/rails/generators/rails/app/app_generator.rb
1
2
3
4
5
6
7
8
9
10
| -
-
-
-
|
|
|
|
|
!
| module Rails
module Generators
class AppGenerator < AppBase def create_root_files
build(:readme)
build(:rakefile)
build(:configru)
build(:gitignore) unless options[:skip_git]
build(:gemfile) unless options[:skip_gemfile]
end
|
明らかにcreate_root_files実行されてる(commandsに入れられてる)よなぁと思ってたらthor/base.rbにいい感じに邪悪なコードがありました:-<
thor/lib/thor/base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| -
|
|
-
|
|
!
|
-
!
|
|
|
|
|
|
!
| def method_added(meth)
meth = meth.to_s
if meth == "initialize"
initialize_added
return
end
return unless public_method_defined?(meth.to_sym)
@no_commands ||= false
return if @no_commands || !create_command(meth)
is_thor_reserved_word?(meth, :command)
Thor::Base.register_klass_file(self)
end
|
というわけで、publicメソッドはcommandsとして登録されるようです(゜Д゜)。publicだけどcommandsに入れたくない場合はno_commands { commandsに入れたくないメソッドを定義 }とかおなか一杯過ぎなので省略します。なお、commandsはOrderedHashなので、commandsはメソッドを定義した順に実行されます。
ここまで来たら後は淡々とテンプレートのファイルをコピーするだけです。正確に言うと、buildメソッド→builder(デフォルトapp_generator.rbに定義されているAppBuilder)のメソッド呼び出し→templateとか(thor/lib/actionsディレクトリにある*.rbに定義されています)を呼び出してファイルコピーを行っています。
おわりに †
今回はRailsアプリケーションのスタート地点、rails newコマンドの実装を見てきました。autoload、include・included・extendと初心者お断り!感満載なコードでした。特にmethod_addedを使ったコマンドの定義はやりすぎかなと思います・・・
rails generateまで読む予定でしたが長くなるので今回はここまで、別ページに改めて書くことにします。
そういえばbundle install忘れてた。けど、長くなるのでやめます。bundleもThor使ってるみたいです。