Ruby on Rails4を読む

はじめに

Rails4では以下のようにアプリケーションのベースとなるファイルを生成するようです。

C:\Sites>rails new foo
C:\Sites>cd foo
C:\Sites\foo>rails generate scaffold user name:string email:string

アプリケーション名やらなんやらはRails入れるときに参考にしたサイトのサンプルそのまんまです。深い意味はありません。

さて、以前に読んだときはファイル生成周りを読んでませんが興味がわかなかったのか読んで萌えポイントがなかったのか、ともかく今回は読みます。ちらっと見ただけで萌えポイントがあったので:-)

PATH上にあるrailsコマンド

まずはエントリーポイント、railsコマンドを見ていきます。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
 
 
 
 
-
!
 
 
require 'rubygems'
 
version = ">= 0"
 
# 引数でバージョン指定があったらversionを更新するコード
 
gem 'railties', version
load Gem.bin_path('railties', 'rails', version)

というわけでrailtiesのrailsに進みます。「.gitがあったら」のコードを除くと以下の一行です。

Everything is expanded.Everything is shortened.
  1
 
require "rails/cli"

railties/lib/rails/cli.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

妙なことが書いてありますがいったん無視して下の方に注目します。(気になる人のために書いておくと「If we are inside Rails application」というのは「Railsアプリケーションディレクトリ内でコマンドが実行されたら」ということです。アプリディレクトリを作るときは当然、アプリディレクトリ内にはいないので下のコードが実行されます)

railties/lib/rails/commands/application.rb

rails/commands/application.rb。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
 
 
 
 
 
 
 
require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
 
(中略)
 
args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
Rails::Generators::AppGenerator.start args

ARGVScrubberはapp_generator.rbの後半に書かれています。いろいろややこしく書いてありますがやっていることは以下の2点です。

  • 変なコマンドが指定されてたら--helpが指定されてたということにする
  • railsrcを読み込んでオプションに加える

さて、AppGenerator.startですがこれを追っかけるのはなかなか面倒です。Railsのソース読むためにはこの追っかけ方法を会得する必要があります:-)

rails/generators/rails/app/app_generator.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
-
-
-
require 'rails/generators/app_base'
 
module Rails
  module Generators
    class AppGenerator < AppBase # :nodoc:

rails/generators/app_base.rb

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

base.rbはrequireしていない。しかし、generators.rbで以下のように書かれている。

rails/generators.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
-
-
|
module Rails
  module Generators
    autoload :Base,            'rails/generators/base'

rails/generators/base.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
 
 
-
-
-
require 'thor/group'
 
module Rails
  module Generators
    class Base < Thor::Group

というわけで、gemが変わってthorに移動します。

thor/group.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
 
 
 
require "thor/base"
 
class Thor::Group # rubocop:disable ClassLength
  include Thor::Base

thor/base.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
-
|
-
-
|
|
|
!
|
-
-
|
|
!
class Thor
  module Base
  
    class << self
      def included(base) #:nodoc:
        base.extend ClassMethods
        base.send :include, Invocation
        base.send :include, Shell
      end
  
    module ClassMethods
      def start(given_args = ARGV, config = {})
        config[:shell] ||= Thor::Base.shell.new
        dispatch(nil, given_args.dup, nil, config)
      end

というわけでようやくstartはけ〜んです。

Thor::Group.dispatch (thor/lib/thor/group.rb)

さてというわけでdispatchというなんかコマンド実行してそうな雰囲気のメソッドにたどり着きました。見出しにもう書いてますがBaseのdispatchは例外投げるだけで実際にはGroupに定義されているdispatchが呼ばれます。

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
-
-
-
-
|
|
!
|
|
|
|
|
|
|
-
|
|
|
!
!
class Thor::Group # rubocop:disable ClassLength
  class << self
    def dispatch(command, given_args, given_opts, config) #:nodoc:
      if Thor::HELP_MAPPINGS.include?(given_args.first)
        help(config[:shell])
        return
      end
 
      args, opts = Thor::Options.split(given_args)
      opts = given_opts || opts
 
      instance = new(args, opts, config)
      yield instance if block_given?
 
      if command
        instance.invoke_command(all_commands[command])
      else
        instance.invoke_all
      end
    end

オプションは付けてないので飛ばします。大したことはやってないので興味があったら見てみてください。

で、newされてます。やや見慣れない感じですが、selfはクラスオブジェクトなので通常通りインスタンス生成が行われます。クラスオブジェクトはAppGeneratorなのでrailtiesの方に戻りましょう。

railties/lib/rails/generators/rails/app/app_generator.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
-
|
|
-
|
!
|
-
|
!
!
class AppGenerator < AppBase # :nodoc:
  def initialize(*args)
    super
 
    unless app_path
      raise Error, "Application name should be provided in arguments. For details run: rails --help"
    end
 
    if !options[:skip_active_record] && !DATABASES.include?(options[:database])
      raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
    end
  end

「app_pathってなに〜!?」って心の叫びを今から解決します。superしてるのでスーパークラスのinitialize呼ばれます。実際にはAppBaseのinitializeが呼ばれてさらにそこからsuperされてますが飛ばしてThor::Baseのinitializeに移ります。

thor/lib/thor/base.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
-
-
!
-
|
|
|
|
!
-
|
-
|
|
!
def initialize(args = [], local_options = {}, config = {}) # rubocop:disable MethodLength
  # オプションの処理。さくっと省略します
 
  # Add the remaining arguments from the options parser to the
  # arguments passed in to initialize. Then remove any positional
  # arguments declared using #argument (this is primarily used
  # by Thor::Group). Tis will leave us with the remaining
  # positional arguments.
  to_parse  = args
  to_parse += opts.remaining unless self.class.strict_args_position?(config)
 
  thor_args = Thor::Arguments.new(self.class.arguments)
  thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) }
  @args = thor_args.remaining
end

また謎来ました。見た感じ、渡された引数を定義されている引数リスト(arguments)に基づいてsetter呼び出してる雰囲気です。とするとapp_pathという引数(argument)を定義しておけば先ほどのAppGenerator#initializeは問題がなくなりそうです。

ところで・・・、新しくアプリケーション作るときって、「rails new foo」でしたよね。ところがぎっちょん、今のargsには['foo']と'new'は除けられた配列になっています。いつの間にそうなったのかというと先ほどのARGVScrubber、

railties/lib/rails/generators/rails/app/app_generator.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
 37
-
|
-
|
!
!
 
-
-
|
|
|
!
!
 
-
-
|
|
|
!
!
 
-
-
|
|
|
-
!
!
|
-
|
|
|
!
def prepare!
  handle_version_request!(@argv.first)
  handle_invalid_command!(@argv.first, @argv) do
    handle_rails_rc!(@argv.drop(1))
  end
end
 
def handle_invalid_command!(argument, argv)
  if argument == "new"
    yield
  else
    ['--help'] + argv.drop(1)
  end
end
 
def handle_rails_rc!(argv)
  if argv.find { |arg| arg == '--no-rc' }
    argv.reject { |arg| arg == '--no-rc' }
  else
    railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
  end
end
 
def railsrc(argv)
  if (customrc = argv.index{ |x| x.include?("--rc=") })
    fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
    yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
  else
    yield argv, self.class.default_rc_file
  end
end
 
def insert_railsrc_into_argv!(argv, railsrc)
  return argv unless File.exist?(railsrc)
  extra_args = read_rc_file railsrc
  argv.take(1) + extra_args + argv.drop(1)
end

渡された引数配列をdrop(1)しているのでしれっとnewが除けられています。はじめ見たときは気づきませんでした。(argumentsまで来たところで、「あれ?newどこ行ったん?」と思いました)

話を戻してargumentです。AppGeneratorにはapp_pathの定義はありませんがスーパークラスのAppBaseにありました。

railties/lib/rails/generators/app_base.rb

Everything is expanded.Everything is shortened.
  1
  2
-
|
class AppBase < Base # :nodoc:
  argument :app_path, type: :string

argumentメソッドを貼るのは省略します。attr_accessorでsetter/getter作ってるのとargumentsにThor::Argumentインスタンスを追加しています。

Thor::Invocation::invoke_all (thor/lib/thor/invocation.rb)

さて。ここまででやっとインスタンス生成が終わりました。ようやくコマンド実行です。invoke_allが呼ばれます。group.rbやbase.rbにinvoke_allない、と思ったら同ディレクトリにinvocation.rbがあってそこにありました。ちなみに、InvocationはBaseのincludedメソッドでincludeされてます・・・

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
-
-
-
-
!
class Thor
  module Invocation
    def invoke_all #:nodoc:
      self.class.all_commands.map { |_, command| invoke_command(command) }
    end

またまた謎のall_commandsが登場です。こちらはbase.rbに書かれています。

thor/lib/thor.base.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
-
|
|
!
 
-
|
!
def all_commands
  @all_commands ||= from_superclass(:all_commands, Thor::CoreExt::OrderedHash.new)
  @all_commands.merge(commands)
end
 
def commands
  @commands ||= Thor::CoreExt::OrderedHash.new
end

まあ想像通りかなと思います。OrderedHashってのはまあOrderedHashなのでしょう。

で・・・、commandsはいつ設定されるの?と先ほどのargument的なのを小一時間探したのですが見当たりませんでした。app_generator.rbを見ると

railties/lib/rails/generators/rails/app/app_generator.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
-
-
-
-
|
|
|
|
|
!
module Rails
  module Generators
    class AppGenerator < AppBase # :nodoc:
      def create_root_files
        build(:readme)
        build(:rakefile)
        build(:configru)
        build(:gitignore) unless options[:skip_git]
        build(:gemfile)   unless options[:skip_gemfile]
      end

明らかにcreate_root_files実行されてる(commandsに入れられてる)よなぁと思ってたらthor/base.rbにいい感じに邪悪なコードがありました:-<

thor/lib/thor/base.rb

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
-
|
|
-
|
|
!
|
-
!
|
|
|
|
|
|
!
def method_added(meth)
  meth = meth.to_s
 
  if meth == "initialize"
    initialize_added
    return
  end
 
  # Return if it's not a public instance method
  return unless public_method_defined?(meth.to_sym)
 
  @no_commands ||= false
  return if @no_commands || !create_command(meth)
 
  is_thor_reserved_word?(meth, :command)
  Thor::Base.register_klass_file(self)
end

というわけで、publicメソッドはcommandsとして登録されるようです(゜Д゜)。publicだけどcommandsに入れたくない場合はno_commands { commandsに入れたくないメソッドを定義 }とかおなか一杯過ぎなので省略します。なお、commandsはOrderedHashなので、commandsはメソッドを定義した順に実行されます。

ここまで来たら後は淡々とテンプレートのファイルをコピーするだけです。正確に言うと、buildメソッド→builder(デフォルトapp_generator.rbに定義されているAppBuilder)のメソッド呼び出し→templateとか(thor/lib/actionsディレクトリにある*.rbに定義されています)を呼び出してファイルコピーを行っています。

おわりに

今回はRailsアプリケーションのスタート地点、rails newコマンドの実装を見てきました。autoload、include・included・extendと初心者お断り!感満載なコードでした。特にmethod_addedを使ったコマンドの定義はやりすぎかなと思います・・・

rails generateまで読む予定でしたが長くなるので今回はここまで、別ページに改めて書くことにします。

そういえばbundle install忘れてた。けど、長くなるのでやめます。bundleもThor使ってるみたいです。


トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2014-08-17 (日) 20:13:31 (3539d)