[[Ruby on Rails4を読む]]
 
 #contents
 
 *はじめに [#y72fe653]
 
 Rails4では以下のようにアプリケーションのベースとなるファイルを生成するようです。
 
  C:\Sites>rails new foo
  C:\Sites>cd foo
  C:\Sites\foo>rails generate scaffold user name:string email:string
 
 アプリケーション名やらなんやらはRails入れるときに[[参考にしたサイト>http://www.catch.jp/wiki/index.php?windows_rails]]のサンプルそのまんまです。深い意味はありません。
 
 さて、以前に読んだときはファイル生成周りを読んでませんが興味がわかなかったのか読んで萌えポイントがなかったのか、ともかく今回は読みます。ちらっと見ただけで萌えポイントがあったので:-)
 
 *PATH上にあるrailsコマンド [#h348e558]
 
 まずはエントリーポイント、railsコマンドを見ていきます。
 
 #code(Ruby){{
 require 'rubygems'
 
 version = ">= 0"
 
 # 引数でバージョン指定があったらversionを更新するコード
 
 gem 'railties', version
 load Gem.bin_path('railties', 'rails', version)
 }}
 
 というわけでrailtiesのrailsに進みます。「.gitがあったら」のコードを除くと以下の一行です。
 
 #code(Ruby){{
 require "rails/cli"
 }}
 
 *railties/lib/rails/cli.rb [#ieb0f093]
 
 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
 }}
 
 妙なことが書いてありますがいったん無視して下の方に注目します。(気になる人のために書いておくと「If we are inside Rails application」というのは「Railsアプリケーションディレクトリ内でコマンドが実行されたら」ということです。アプリディレクトリを作るときは当然、アプリディレクトリ内にはいないので下のコードが実行されます)
 
 *railties/lib/rails/commands/application.rb [#ja65951d]
 
 rails/commands/application.rb。
 
 #code(Ruby){{
 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
 #code(Ruby){{
 require 'rails/generators/app_base'
 
 module Rails
   module Generators
     class AppGenerator < AppBase # :nodoc:
 }}
 
 rails/generators/app_base.rb
 #code(Ruby){{
 require 'rails/generators'
 
 module Rails
   module Generators
     class AppBase < Base # :nodoc:
 }}
 
 base.rbはrequireしていない。しかし、generators.rbで以下のように書かれている。
 
 rails/generators.rb
 #code(Ruby){{
 module Rails
   module Generators
     autoload :Base,            'rails/generators/base'
 }}
 
 rails/generators/base.rb
 #code(Ruby){{
 require 'thor/group'
 
 module Rails
   module Generators
     class Base < Thor::Group
 }}
 
 というわけで、gemが変わってthorに移動します。
 
 thor/group.rb
 #code(C){{
 require "thor/base"
 
 class Thor::Group # rubocop:disable ClassLength
   include Thor::Base
 }}
 
 thor/base.rb
 #code(Ruby){{
 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) [#v72b37fc]
 
 さてというわけでdispatchというなんかコマンド実行してそうな雰囲気のメソッドにたどり着きました。見出しにもう書いてますがBaseのdispatchは例外投げるだけで実際にはGroupに定義されているdispatchが呼ばれます。
 
 #code(Ruby){{
 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
 #code(Ruby){{
 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
 #code(Ruby){{
 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
 #code(Ruby){{
 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
 #code(Ruby){{
 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) [#ff1139d1]
 
 さて。ここまででやっとインスタンス生成が終わりました。ようやくコマンド実行です。invoke_allが呼ばれます。group.rbやbase.rbにinvoke_allない、と思ったら同ディレクトリにinvocation.rbがあってそこにありました。ちなみに、InvocationはBaseのincludedメソッドでincludeされてます・・・
 
 #code(Ruby){{
 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
 #code(Ruby){{
 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
 #code(Ruby){{
 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
 #code(Ruby){{
 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に定義されています)を呼び出してファイルコピーを行っています。
 
 *おわりに [#ce896bb4]
 
 今回はRailsアプリケーションのスタート地点、rails newコマンドの実装を見てきました。autoload、include・included・extendと初心者お断り!感満載なコードでした。特にmethod_addedを使ったコマンドの定義はやりすぎかなと思います・・・
 
 rails generateまで読む予定でしたが長くなるので今回はここまで、別ページに改めて書くことにします。
 
 そういえばbundle install忘れてた。けど、長くなるのでやめます。bundleもThor使ってるみたいです。
 

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