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です。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 
 
-
|
!
 
 
 
 
-
|
|
|
|
!
require 'rails/app_rails_loader'
 
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
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に進みます。

Everything is expanded.Everything is shortened.
  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 # non reachable, hack to be able to stub exec in the test suite
          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
 
        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
 
        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end
 
    def self.find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end

何しているかというと、アプリケーションディレクトリのbin/railsを実行しています。<APP_DIR>/bin/railsは以下のようになっています。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
!
 
 
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require 'rails/commands'

railties/lib/rails/commands.rb)

全部見ると長くなるのでrails/commandsに進みます。

Everything is expanded.Everything is shortened.
  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へ。

Everything is expanded.Everything is shortened.
  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 # :nodoc:
    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

Everything is expanded.Everything is shortened.
  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

Everything is expanded.Everything is shortened.
  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です)からクラスを探しているようです。とりあえずコード。

Everything is expanded.Everything is shortened.
  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) #:nodoc:
  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"]です。

Everything is expanded.Everything is shortened.
  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) #:nodoc:
  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) #:nodoc:
  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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
-
-
-
require 'rails/generators/rails/resource/resource_generator'
 
module Rails
  module Generators
    class ScaffoldGenerator < ResourceGenerator # :nodoc:

rails/generators/rails/resource/resource_generator.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
-
-
-
module Rails
  module Generators
    class ResourceGenerator < ModelGenerator # :nodoc:

rails/generators/rails/model/model_generator.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
-
-
|
module Rails
  module Generators
    class ModelGenerator < NamedBase # :nodoc:
      argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"

rails/generators/named_base.rb

Everything is expanded.Everything is shortened.
  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

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
-
-
-
|
module Rails
  module Generators
    class ScaffoldGenerator < ResourceGenerator # :nodoc:
      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に定義されています。

Everything is expanded.Everything is shortened.
  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で定義されています。

Everything is expanded.Everything is shortened.
  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) # rubocop:disable MethodLength
  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でオーバーライドされていました。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
|
|
-
|
|
|
|
|
|
!
!
def self.prepare_for_invocation(name, value) #:nodoc:
  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、並びにスーパークラスを眺めるとスーパークラスに定義されたもの(メソッド定義が早い順)に実行されていることがわかります。

Everything is expanded.Everything is shortened.
  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 # :nodoc:
  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 # :nodoc:
  hook_for :resource_controller, required: true do |controller|
    invoke controller, [ controller_name, options[:actions] ]
  end
 
  hook_for :resource_route, required: true
 
class ModelGenerator < NamedBase # :nodoc:
  hook_for :orm, required: true

で、hook_for :ormと書いてあるのになんでactive_recordが呼ばれてんの?

とりあえずormでgrepかけるとrails/engine.rbに以下のコメントが見つかります。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# == Generators
#
# You can set up generators for engines with <tt>config.generators</tt> method:
#
#   class MyEngine < Rails::Engine
#     config.generators do |g|
#       g.orm             :active_record
#       g.template_engine :erb
#       g.test_framework  :test_unit
#     end
#   end
#
# You can also set generators for an application by using <tt>config.app_generators</tt>:
#
#   class MyEngine < Rails::Engine
#     # note that you can also pass block to app_generators in the same way you
#     # can pass it to generators method
#     config.app_generators.orm :datamapper
#   end

普通に考えるとormのgeneratorとしてactive_recordが設定されているということになります。もう、「以上、終わり!」としたいとこですが、悲しいけどこれコードリーディング記事なのよね、ってことで、ではコメントではなく実際なとこ何処でorm = active_recordが設定されているのかを探してみたいと思います。

gems全体をgrepすると以下のコードが引っ掛かります。

activerecord/lib/active_record/railtie.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
-
-
-
|
|
|
|
module ActiveRecord
  # = Active Record Railtie
  class Railtie < Rails::Railtie # :nodoc:
    config.active_record = ActiveSupport::OrderedOptions.new
 
    config.app_generators.orm :active_record, :migration => true,
                                              :timestamps => true

見つかりました。ちなみにこのファイルは前半読み飛ばした以下のところでrequireされています。

rails/commands/commands_tasks

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
-
-
-
|
|
|
|
!
|
-
|
|
!
module Rails
  class CommandsTasks # :nodoc:
    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

Everything is expanded.Everything is shortened.
  1
 
APP_PATH = File.expand_path('../../config/application',  __FILE__)

<APP_DIR>/config/applications

Everything is expanded.Everything is shortened.
  1
 
require 'rails/all'

railties/lib/rails/all.rb

Everything is expanded.Everything is shortened.
  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を拡張する仕組み(基本セットも同じ概念上に乗っている)のようです。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
|
|
|
|
|
|
|
# Railtie is the core of the Rails framework and provides several hooks to extend
# Rails and/or modify the initialization process.
#
# Every major component of Rails (Action Mailer, Action Controller,
# Action View and Active Record) is a Railtie. Each of
# them is responsible for their own initialization. This makes Rails itself
# absent of any component hooks, allowing other components to be used in
# place of any of the Rails defaults.

かなり長くなってきましたがまだ続きます。

railties/lib/rails/railtie.rb

Everything is expanded.Everything is shortened.
  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

Everything is expanded.Everything is shortened.
  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です。

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


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS