Ruby on Rails4を読む
はじめに †
前回はrails new時の挙動について見てきました。railsコマンドは初めにアプリケーションを作成するとき以外にも
rails generate scaffold user name:string email:string
のように使用します。今回はこの場合にどういう処理が行われるのか見ていきます。
Rails::AppRailsLoader (railties/lib/rails/app_rails_loader.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
|
前回は下部のrails/commands/application.rbに進みました。今回は前回無視したapp_rails_loader.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
| -
-
|
|
|
-
|
|
-
-
|
|
-
|
|
|
|
|
|
|
|
!
!
|
-
|
!
|
-
!
!
!
|
-
|
!
| module Rails
module AppRailsLoader
RUBY = Gem.ruby
EXECUTABLES = ['bin/rails', 'script/rails']
def self.exec_app_rails
original_cwd = Dir.pwd
loop do
if exe = find_executable
contents = File.read(exe)
if contents =~ /(APP|ENGINE)_PATH/
exec RUBY, exe, *ARGV
break elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
$stderr.puts(BUNDLER_WARNING)
Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
require File.expand_path('../boot', APP_PATH)
require 'rails/commands'
break
end
end
Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
Dir.chdir('..')
end
end
def self.find_executable
EXECUTABLES.find { |exe| File.file?(exe) }
end
|
何しているかというと、アプリケーションディレクトリのbin/railsを実行しています。<APP_DIR>/bin/railsは以下のようになっています。
1
2
3
4
| -
!
| APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
|
railties/lib/rails/commands.rb) †
全部見ると長くなるのでrails/commandsに進みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
| ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
}
command = ARGV.shift
command = aliases[command] || command
require 'rails/commands/commands_tasks'
Rails::CommandsTasks.new(ARGV).run_command!(command)
|
commands_tasks.rbへ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| -
-
-
|
-
|
|
|
!
!
|
-
|
!
|
-
|
|
|
|
!
| module Rails
class CommandsTasks def run_command!(command)
command = parse_command(command)
if COMMAND_WHITELIST.include?(command)
send(command)
else
write_error_message(command)
end
end
def generate
generate_or_destroy(:generate)
end
def generate_or_destroy(command)
require 'rails/generators'
require_application_and_environment!
Rails.application.load_generators
require "rails/commands/#{command}"
end
|
メソッド呼び出しとかrequireとかしてて知らぬ間にいろいろ設定されてそうですがぱっと見ではよくわからないのでgenerate.rbに進みます。
railties/lib/rails/commands/generate.rb
1
2
3
4
5
6
7
8
9
10
11
|
-
|
|
!
| require 'rails/generators'
if [nil, "-h", "--help"].include?(ARGV.first)
Rails::Generators.help 'generate'
exit
end
name = ARGV.shift
root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
Rails::Generators.invoke name, ARGV, behavior: :invoke, destination_root: root
|
忘れかけているので確認。この時点でのARGVは
scaffold user name:string email:string
です。
invokeメソッドに進む。
railties/lib/rails/generators.rb
1
2
3
4
5
6
7
8
9
| -
|
-
|
|
|
|
!
!
| def self.invoke(namespace, args=ARGV, config={})
names = namespace.to_s.split(':')
if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
klass.start(args, config)
else
puts "Could not find generator #{namespace}."
end
end
|
find_by_namespace (railties/lib/rails/generators.rb) †
そろそろ見出しを変えます。与えられたname(コマンド。namespaceもサポートしてるようだけど今回はnamespaceなしのscaffoldです)からクラスを探しているようです。とりあえずコード。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| -
|
|
|
|
-
-
|
|
!
|
!
|
|
|
|
|
-
|
|
!
|
|
!
| def self.find_by_namespace(name, base=nil, context=nil) lookups = []
lookups << "#{base}:#{name}" if base
lookups << "#{name}:#{context}" if context
unless base || context
unless name.to_s.include?(?:)
lookups << "#{name}:#{name}"
lookups << "rails:#{name}"
end
lookups << "#{name}"
end
lookup(lookups)
namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
lookups.each do |namespace|
klass = namespaces[namespace]
return klass if klass
end
invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
end
|
loookupsは["scaffold/scaffold", "rails/scaffold", "scaffold"]です。
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
| -
|
|
-
-
|
|
-
|
|
|
|
|
|
!
!
!
!
-
|
-
|
|
|
!
|
|
!
| def self.lookup(namespaces) paths = namespaces_to_paths(namespaces)
paths.each do |raw_path|
["rails/generators", "generators"].each do |base|
path = "#{base}/#{raw_path}_generator"
begin
require path
return
rescue LoadError => e
raise unless e.message =~ /#{Regexp.escape(path)}$/
rescue Exception => e
warn "[WARNING] Could not load generator #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
end
end
end
end
def self.namespaces_to_paths(namespaces) paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
paths << pieces.dup.push(pieces.last).join("/")
paths << pieces.join("/")
end
paths.uniq!
paths
end
|
最終的に、rails/generators/rails/scaffold/scaffold_generator.rbがrequireされます。
railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb †
1
2
3
4
5
|
-
-
-
| require 'rails/generators/rails/resource/resource_generator'
module Rails
module Generators
class ScaffoldGenerator < ResourceGenerator
|
rails/generators/rails/resource/resource_generator.rb
1
2
3
| -
-
-
| module Rails
module Generators
class ResourceGenerator < ModelGenerator
|
rails/generators/rails/model/model_generator.rb
1
2
3
4
| -
-
-
|
| module Rails
module Generators
class ModelGenerator < NamedBase argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
|
rails/generators/named_base.rb
1
2
3
4
| -
-
-
|
| module Rails
module Generators
class NamedBase < Base
argument :name, type: :string
|
というわけで、前回見たRails::Generator::Baseまで来ました。NamedBaseでname、ModelGeneratorでattributesが定義されているので、
name: "user"
arguments: ["name:string", "email:string"]
が設定されます。
さてでは次に実行されるコマンドを・・・と見てみてもそれっぽい定義はありません。代わりにhook_forという怪しいメソッドが呼ばれています。
railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
1
2
3
4
| -
-
-
|
| module Rails
module Generators
class ScaffoldGenerator < ResourceGenerator hook_for :scaffold_controller, required: true
|
rails/generators/rails/scaffold_controller/template/controller.rbを見ると明らかにこれが使われていそうです。というわけで以降はどういうからくりでhook_forで登録したものが呼ばれているのかを調べていきたいと思います。
= hook_for (railties/lib/rails/generator/base.rb)
hook_forはRails::Generator::Baseに定義されています。
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
38
39
| -
|
|
|
|
-
-
|
|
|
|
|
|
!
|
|
!
|
|
|
!
-
-
-
|
!
!
!
-
-
-
|
|
!
!
!
| def self.hook_for(*names, &block)
options = names.extract_options!
in_base = options.delete(:in) || base_name
as_hook = options.delete(:as) || generator_name
names.each do |name|
unless class_options.key?(name)
defaults = if options[:type] == :boolean
{ }
elsif [true, false].include?(default_value_for_option(name, options))
{ banner: "" }
else
{ desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
end
class_option(name, defaults.merge!(options))
end
hooks[name] = [ in_base, as_hook ]
invoke_from_option(name, options, &block)
end
end
def self.base_name
@base_name ||= begin
if base = name.to_s.split('::').first
base.underscore
end
end
end
def self.generator_name
@generator_name ||= begin
if generator = name.to_s.split('::').last
generator.sub!(/Generator$/, '')
generator.underscore
end
end
end
|
base_name、generator_nameで使われているnameはクラス名を表す文字列なので注意してください。それぞれ、"rails"、"scaffold"になります。
invoke_from_optionはThor::Groupで定義されています。
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
| -
|
|
|
-
-
|
|
!
|
|
|
|
|
-
|
|
|
|
-
|
-
|
-
|
|
|
!
!
|
!
!
| def invoke_from_option(*names, &block) options = names.last.is_a?(Hash) ? names.pop : {}
verbose = options.fetch(:verbose, :white)
names.each do |name|
unless class_options.key?(name)
fail ArgumentError, "You have to define the option #{name.inspect} " <<
"before setting invoke_from_option."
end
invocations[name] = true
invocation_blocks[name] = block if block_given?
class_eval <<-METHOD, __FILE__, __LINE__
def _invoke_from_option_#{name.to_s.gsub(/\W/, "_")}
return unless options[#{name.inspect}]
value = options[#{name.inspect}]
value = #{name.inspect} if TrueClass === value
klass, command = self.class.prepare_for_invocation(#{name.inspect}, value)
if klass
say_status :invoke, value, #{verbose.inspect}
block = self.class.invocation_blocks[#{name.inspect}]
_invoke_for_class_method klass, command, &block
else
say_status :error, %(\#{value} [not found]), :red
end
end
METHOD
end
end
|
一目見ただけで邪悪さ120%なコードです。このメソッドが実行されるとnameに応じた_invoke_from_options_#{name}というメソッドが定義されます。で、前回から読んでいただいてる人はご存知だと思いますが、publicなメソッドはコマンドとして実行されるので、hook_forを記述することにより実行されるコマンドの定義ができたということになります。
prepare_for_invocationはnameで指定されたものに対応するクラスを探しています。このメソッドはthor/invocation.rbに書かれていて・・・、というのは引っかけです。私も初めthorの方にあるprepare_for_invocationを(grepで引っかけて)見ていたのですがどう読み進めても先に進めない(scaffold_controllerが実行されない)のでrailtiesでgrepしたところ、rails/generator/base.rbでオーバーライドされていました。
1
2
3
4
5
6
7
8
9
10
11
12
| -
|
|
-
|
|
|
|
|
|
!
!
| def self.prepare_for_invocation(name, value) return super unless value.is_a?(String) || value.is_a?(Symbol)
if value && constants = self.hooks[name]
value = name if TrueClass === value
Rails::Generators.find_by_namespace(value, *constants)
elsif klass = Rails::Generators.find_by_namespace(value)
klass
else
super
end
end
|
ifは一番上が実行されます。まあともかくscaffold_controller_generator.rbにたどり着きます。
orm †
ところでここでrails generate scaffoldの出力を見てみましょう。
rails generate scaffold user name:string email:string
invoke active_record
create db/migrate/20140809033025_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
invoke resource_route
route resources :users
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke erb
create app/views/users
create app/views/users/index.html.erb
create app/views/users/edit.html.erb
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/_form.html.erb
invoke test_unit
create test/controllers/users_controller_test.rb
invoke helper
create app/helpers/users_helper.rb
invoke test_unit
create test/helpers/users_helper_test.rb
invoke jbuilder
create app/views/users/index.json.jbuilder
create app/views/users/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/users.js.coffee
invoke scss
create app/assets/stylesheets/users.css.scss
invoke scss
create app/assets/stylesheets/scaffolds.css.scss
invokeというのはgenerator呼んでいるということでしょう。またインデントはgeneratorのネストを表しているのでしょう。
さて、上記の出力結果とScaffoldGenerator、並びにスーパークラスを眺めるとスーパークラスに定義されたもの(メソッド定義が早い順)に実行されていることがわかります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| -
|
|
|
|
-
|
!
|
-
-
|
!
!
|
-
-
|
!
|
|
|
-
|
| class ScaffoldGenerator < ResourceGenerator remove_hook_for :resource_controller
hook_for :scaffold_controller, required: true
hook_for :assets do |assets|
invoke assets, [controller_name]
end
hook_for :stylesheet_engine do |stylesheet_engine|
if behavior == :invoke
invoke stylesheet_engine, [controller_name]
end
end
class ResourceGenerator < ModelGenerator hook_for :resource_controller, required: true do |controller|
invoke controller, [ controller_name, options[:actions] ]
end
hook_for :resource_route, required: true
class ModelGenerator < NamedBase hook_for :orm, required: true
|
で、hook_for :ormと書いてあるのになんでactive_recordが呼ばれてんの?
とりあえずormでgrepかけるとrails/engine.rbに以下のコメントが見つかります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
普通に考えるとormのgeneratorとしてactive_recordが設定されているということになります。もう、「以上、終わり!」としたいとこですが、悲しいけどこれコードリーディング記事なのよね、ってことで、ではコメントではなく実際なとこ何処でorm = active_recordが設定されているのかを探してみたいと思います。
gems全体をgrepすると以下のコードが引っ掛かります。
activerecord/lib/active_record/railtie.rb
1
2
3
4
5
6
7
| -
-
-
|
|
|
|
| module ActiveRecord
class Railtie < Rails::Railtie config.active_record = ActiveSupport::OrderedOptions.new
config.app_generators.orm :active_record, :migration => true,
:timestamps => true
|
見つかりました。ちなみにこのファイルは前半読み飛ばした以下のところでrequireされています。
rails/commands/commands_tasks
1
2
3
4
5
6
7
8
9
10
11
12
13
| -
-
-
|
|
|
|
!
|
-
|
|
!
| module Rails
class CommandsTasks def generate_or_destroy(command)
require 'rails/generators'
require_application_and_environment!
Rails.application.load_generators
require "rails/commands/#{command}"
end
def require_application_and_environment!
require APP_PATH
Rails.application.require_environment!
end
|
<APP_DIR>/bin/rails
1
|
| APP_PATH = File.expand_path('../../config/application', __FILE__)
|
<APP_DIR>/config/applications
railties/lib/rails/all.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-
-
|
|
!
!
| require "rails"
%w(
active_record
action_controller
action_view
action_mailer
rails/test_unit
sprockets
).each do |framework|
begin
require "#{framework}/railtie"
rescue LoadError
end
end
|
今まで気にせずにrsiltieという単語を使ってきましたが、railtieというのはRailsを拡張する仕組み(基本セットも同じ概念上に乗っている)のようです。
1
2
3
4
5
6
7
8
| -
|
|
|
|
|
|
|
|
|
かなり長くなってきましたがまだ続きます。
railties/lib/rails/railtie.rb
1
2
3
4
5
6
7
8
9
10
11
12
| -
-
-
|
|
-
|
!
|
-
|
!
| module Rails
class Railtie
class << self
delegate :config, to: :instance
def instance
@instance ||= new
end
def config
@config ||= Railtie::Configuration.new
end
|
railties/lib/rails/railtie/configuration.rb
1
2
3
4
5
6
7
8
| -
-
-
-
|
|
|
!
| module Rails
class Railtie
class Configuration
def app_generators
@@app_generators ||= Rails::Configuration::Generators.new
yield(@@app_generators) if block_given?
@@app_generators
end
|
ralties/lib/rails/configuration.rb
1
2
3
| module Rails
module Configuration
class Generators #:nodoc:
|
さてと、Rails::Configuration::Generatorsまで来ましたが、ormなんてメソッドありません。まあ、どんな拡張が来るかもわからないのにメソッドなんて用意できませんよね。ではどうするか、そう、みんな大好きmethod_missingです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -
|
|
|
|
-
|
|
|
|
|
!
|
-
|
|
|
!
!
| def method_missing(method, *args)
method = method.to_s.sub(/=$/, '').to_sym
return @options[method] if args.empty?
if method == :rails || args.first.is_a?(Hash)
namespace, configuration = method, args.shift
else
namespace, configuration = args.shift, args.shift
namespace = namespace.to_sym if namespace.respond_to?(:to_sym)
@options[:rails][method] = namespace
end
if configuration
aliases = configuration.delete(:aliases)
@aliases[namespace].merge!(aliases) if aliases
@options[namespace].merge!(configuration)
end
end
|
というわけで、@options[:rails][:orm]として:active_recordが設定されます。
これで、ormに対してactive_recordが使われるようになりました、・・・というには実はまだもう少し関連付けられているところの確認ができてないのですが、OReMoutsukaretayo、ということで今回はここまでにします。
おわりに †
というわけでrails generate時の動作を見てきました。今回も黒魔術満載でした。名前に対してファイル探してrequireするとか、コマンドとして呼ばれるpublicメソッドを動的に生成しているとか、伝家の宝刀method_missingとか。
ともかくこれでファイルの生成は終わりました。次回は「rake db:migrate」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います、多分。