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」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います、多分。