[[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) [#m8d60d0c]
 
 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されています。
 
 railties/lib/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」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います、多分。
 ともかくこれでファイルの生成は終わりました。次回は「rake db:migrate」を見ていきます。これは・・・、多分、今回のよりはましなコードになってると思います。

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS