[[Ruby on Rails4を読む]]

#contents

*はじめに [#n73ec655]

前回はrails new時の挙動について見てきました。railsコマンドは初めにアプリケーションを作成するとき以外にも

  rails generate scaffold user name:string email:string

のように使用します。今回はこの場合にどういう処理が行われるのか見ていきます。

*Rails::AppRailsLoader (railties/lib/rails/app_rails_loader.rb) [#j469bed6]

さて、前回も見たrails/cli.rbです。

#code(Ruby){{
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に進みます。

#code(Ruby){{
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は以下のようになっています。

#code(Ruby){{
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require 'rails/commands'
}}

*railties/lib/rails/commands.rb) [#vc7d5786]

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

#code(Ruby){{
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へ。

#code(Ruby){{
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
#code(Ruby){{
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
#code(Ruby){{
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) [#q4ea6b8b]

そろそろ見出しを変えます。与えられたname(コマンド。namespaceもサポートしてるようだけど今回はnamespaceなしのscaffoldです)からクラスを探しているようです。とりあえずコード。

#code(Ruby){{
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"]です。

#code(Ruby){{
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 [#n0c3bbf2]

#code(Ruby){{
require 'rails/generators/rails/resource/resource_generator'

module Rails
  module Generators
    class ScaffoldGenerator < ResourceGenerator # :nodoc:
}}

rails/generators/rails/resource/resource_generator.rb
#code(Ruby){{
module Rails
  module Generators
    class ResourceGenerator < ModelGenerator # :nodoc:
}}

rails/generators/rails/model/model_generator.rb
#code(Ruby){{
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
#code(Ruby){{
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
#code(Ruby){{
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に定義されています。

#code(Ruby){{
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で定義されています。

#code(Ruby){{
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}というメソッドが定義されます。で、[[前回>Ruby on Rails4/ファイル生成周りを読む/rails new時に行われる処理]]から読んでいただいてる人はご存知だと思いますが、publicなメソッドはコマンドとして実行されるので、hook_forを記述することにより実行されるコマンドの定義ができたということになります。

prepare_for_invocationはnameで指定されたものに対応するクラスを探しています。このメソッドはthor/invocation.rbに書かれていて・・・、というのは引っかけです。私も初めthorの方にあるprepare_for_invocationを(grepで引っかけて)見ていたのですがどう読み進めても先に進めない(scaffold_controllerが実行されない)のでrailtiesでgrepしたところ、rails/generator/base.rbでオーバーライドされていました。

#code(Ruby){{
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 [#o94b4b7b]

ところでここで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、並びにスーパークラスを眺めるとスーパークラスに定義されたもの(メソッド定義が早い順)に実行されていることがわかります。

#code(Ruby){{
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に以下のコメントが見つかります。

#code(Ruby){{
# == 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
#code(Ruby){{
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
#code(Ruby){{
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
}}

&lt;APP_DIR&gt;/bin/rails
#code(Ruby){{
APP_PATH = File.expand_path('../../config/application',  __FILE__)
}}

&lt;APP_DIR&gt;/config/applications
#code(Ruby){{
require 'rails/all'
}}

railties/lib/rails/all.rb
#code(Ruby){{
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を拡張する仕組み(基本セットも同じ概念上に乗っている)のようです。

#code(Ruby){{
# 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
#code(Ruby){{
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
#code(Ruby){{
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
#code(Ruby)){{
module Rails
  module Configuration
    class Generators #:nodoc:
}}

さてと、Rails::Configuration::Generatorsまで来ましたが、ormなんてメソッドありません。まあ、どんな拡張が来るかもわからないのにメソッドなんて用意できませんよね。ではどうするか、そう、みんな大好きmethod_missingです。

#code(Ruby){{
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、ということで今回はここまでにします。

*おわりに [#c43367ae]

というわけでrails generate時の動作を見てきました。今回も黒魔術満載でした。名前に対してファイル探してrequireするとか、コマンドとして呼ばれるpublicメソッドを動的に生成しているとか、伝家の宝刀method_missingとか。

ともかくこれでファイルの生成は終わりました。次回は「rake db:migrate」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います、多分。


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