2011年12月27日火曜日

Railsのソースを読んでみる#4 modelの生成部分

今回みるのはrailtiesの「rails/commands.rb」なのだ。

これは受け取ったコマンドに応じたcase文がつらつらと続いているのだけれど、
まずは「rails g model User name:string email:string」とした場合を想定してみましょう。

コマンドにg(generate)を指定した場合はcommands.rbの28行目の

28 require "rails/commands/#{command}"

が実行されて「rails/commands/generate」が読み込まれます。
で、そのgenerate.rbの中身が以下の通り。

1 require 'rails/generators'
2 require 'active_support/core_ext/object/inclusion'
3
4 Rails::Generators.configure!
5
6 if ARGV.first.in?([nil, "-h", "--help"])
7 Rails::Generators.help 'generate'
8 exit
9 end
10
11 name = ARGV.shift
12 Rails::Generators.invoke name, ARGV, :behavior => :invoke, :destination_root => Rails.root

これだけ。
先の例で言うと11行目で変数nameに「model」が格納された状態で
Rails::Generators.invoke が呼び出されています。
そのメソッドの在処は1つ上の階層の「rails/generators.rb」です。

164 def self.invoke(namespace, args=ARGV, config={})
165 names = namespace.to_s.split(':')
166 if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
167 args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
168 klass.start(args, config)
169 else
170 puts "Could not find generator #{namespace}."
171 end
172 end

166行目でfind_by_namespace("model")で呼び出します。
この呼び出しの結果、「Rails::Generators::ModelGenerator」が帰ってきます。
というわけで168行目にて「Rails::Generators::ModelGenerator」のstartを呼んでいます。
このstartメソッドはどこにあるのだろうか、というわけで「rails console」を立ち上げて聞いてみましょう。

require 'rails/generators'
require 'rails/generators/rails/model/model_generators'
Rails::Generators::ModelGenerator.ancestors

上記を実行するとこんな結果が返ってきます。

[Rails::Generators::ModelGenerator, Rails::Generators::NamedBase, Rails::Generators::Base, Rails::Generators::Actions, Thor::Actions, Thor::Group, Thor::Shell, Thor::Invocation, Thor::Base, Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]

今いるrailtiesの中ではgrepしても該当のstartメソッドが無さそうなので「Thor」と呼ばれるモジュールにありそうです。
そもそも「Thor」とは何ぞや。
次回はそこら辺りを調べてみましょう。

2011年12月17日土曜日

Railsのソースを読んでみる#3 もう1つのrails

今回は「rails new ****」として出来たディレクトリの「アプリ名/script/rails」を見て行きましょう。
ちなみちRails3.1.3です。

1 #!/usr/bin/env ruby
2 # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 APP_PATH = File.expand_path('../../config/application', __FILE__)
4 require File.expand_path('../../config/boot', __FILE__)
5 require 'rails/commands'

3行目では「アプリ名/config/application」というパスをAPP_PATHに格納しています。
そして「アプリ名/config/boot.rb」を読み込み、「rails/commands」(こちらはrailtieの方)を読み込んでいます。

まずは「boot.rb」

1 require 'rubygems'
2
3 # Set up gems listed in the Gemfile.
4 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5
6 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])

ここでは環境変数「BUNDLE_GEMFILE」に「アプリ名/Gemfile」を格納しています。
そして「bundler/setup」を読み込みます。
ここでやっているのはファイル名から分かるとうりGemfileで定義した各種モジュールを読み込んでいます。
bundler/setup.rb にて「Bundler.setup」をコールします。これは1つうえの階層の「bundler.rb」にあります。
こちらを読んでいきましょう。

101 def setup(*groups)
102 # Just return if all groups are already loaded
103 return @setup if defined?(@setup)
104
105 if groups.empty?
106 # Load all groups, but only once
107 @setup = load.setup
108 else
109 @completed_groups ||= []
110 # Figure out which groups haven't been loaded yet
111 unloaded = groups - @completed_groups
112 # Record groups that are now loaded
113 @completed_groups = groups
114 # Load any groups that are not yet loaded
115 unloaded.any? ? load.setup(*unloaded) : load
116 end
117 end

この時点ではgroupsは指定しないし@setupも空なので107行目が実行されます。
loadというのはRuntimeクラスのインスタンスを返してくるメソッドで、そのsetupメソッドを呼んでいます。
そのRuntimeというのはどっから来るんだ、というと同ファイルのもっと上の方にあります。

18 autoload :Definition, 'bundler/definition'
19 autoload :Dependency, 'bundler/dependency'
20 autoload :Dsl, 'bundler/dsl'
21 autoload :Environment, 'bundler/environment'
22 autoload :GemHelper, 'bundler/gem_helper'
23 autoload :Graph, 'bundler/graph'
24 autoload :Index, 'bundler/index'
25 autoload :Installer, 'bundler/installer'
26 autoload :LazySpecification, 'bundler/lazy_specification'
27 autoload :LockfileParser, 'bundler/lockfile_parser'
28 autoload :RemoteSpecification, 'bundler/remote_specification'
29 autoload :Resolver, 'bundler/resolver'
30 autoload :Runtime, 'bundler/runtime'
31 autoload :Settings, 'bundler/settings'
32 autoload :SharedHelpers, 'bundler/shared_helpers'
33 autoload :SpecSet, 'bundler/spec_set'
34 autoload :Source, 'bundler/source'
35 autoload :Specification, 'bundler/shared_helpers'
36 autoload :UI, 'bundler/ui'

autoloadというのは最初の引数に指定した名前を参照したタイミングでrequireするメソッドです。
なので「Runtime.new」とされた時点で30行目の「bundler/runtime」が読み込まれます。
この中ではGemfileで指定しているファイルを順次ロードしているだけなので詳細はパス。

「rails/commands」の方は次回見てみるのだ。

2011年12月11日日曜日

Railsのソースを読んでみる#2 railtie入り口

今回からはrailsのコアである「railties」のソースを追いかけてみるのだ。
「railties-3.1.0/lib/rails/cli.rb」を追いかける。


1 require 'rbconfig'
2 require 'rails/script_rails_loader'
3
4 # If we are inside a Rails application this method performs an exec and thus
5 # the rest of this script is not run.
6 Rails::ScriptRailsLoader.exec_script_rails!
7
8 require 'rails/ruby_version_check'
9 Signal.trap("INT") { puts; exit(1) }
10
11 if ARGV.first == 'plugin'
12 ARGV.shift
13 require 'rails/commands/plugin_new'
14 else
15 require 'rails/commands/application'
16 end

まず「rbconfig」と「rails/script_rails_loader」を読み込んでいます。
6行目では「script/rails」が存在すれば、つまり「rails new」でアプリケーションを生成した状態で
実行した場合はそちらのrailsコマンドを実行します。

8,9行目ではrailsを動かせるrubyのバージョンであるかチェックしています。

11行目以降では「rails plugin」と指定した場合は「rails/commands/plugin_new」を、
それ以外は「application/commands/application」を読み込んでいます。
「rails plugin new Hoge」とすればプラグイン開発用の雛形が生成されるのだ。

11行目以降の部分はrailsアプリを新たに作成する場合に実行されるコードです。
そちらはちょっとパスするとして、生成したあとの「script/rails」を読んでいきましょう。
それは次回。

2011年12月9日金曜日

Railsのソースを読んでみる#1 railsコマンド

「メタプログラミングRuby」という書籍を読んだのだけど、一刻も早く実践しなければ全力で記憶から消え去りそうな気配がありすぎるので何かつくろうと思ったのだけど何も思い浮かばないのでRailsのソースでも見てみようか、と思い立ったのだ。

Linuxカーネルのソース読書を数年前に断念した前科があるけれど、いいの。
今回は最初から全部読む気ないから。
ピックアップ的に気になるところを読んでみよう。

Railsのソースは普通にインストールすれば
/usr/lib/ruby/gems/1.8/gems
にある(Macの場合)。
上記はRuby1.8の例なので適宜読み替えてください。

でもrvmなんかを使っていると場所が変わっているので、rvmで目的のverに切り替えてから「which rails」とすれば実行パスが出てくるので、その周辺にたぶんあります。

私の例では以下のパスでした。
~/.rvm/gems/ruby-1.9.2-p290/gems

でもどっから見たらいいのかよう分からん。
とりあえずrailsのはbinディレクトリしかないのでとりあえずrailsコマンドの中身を見てみる。


1 #!/usr/bin/env ruby
2
3 if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git'))
4 railties_path = File.expand_path('../../railties/lib', __FILE__)
5 $:.unshift(railties_path)
6 end
7 require "rails/cli"


3行目では「gems」に「.git」があるか確認しています。
あれば「XXX/gems/railties/lib」をrailties_pathに格納します。
「$:」はロードパスを示す組み込み変数で「$LOAD_PATH」としても同じです。
5行目ではそのロードパスの先頭にrailties_pathを挿入しています。
7行目では「railities/lib/rails/cli.rb」を読み込んでいます。

要するに「railties/lib/rails/cli.rb」を読んでいるだけなんだけど、最初に「.git」の有無をチェックしているのはなぜだろう。
存在した場合だけロードパスにrailtiesのパスを先頭に追加している。
ググってみても分からん。
とりあえず課題として置いておこう。

とにもかくにもここから分かるのはRailsの一番核になっているのが
railtieだということ。

ここら辺りで予習しておくと良いでしょう。

2011年12月1日木曜日

[ruby]includeとextend

Rubyにおけるincludeとextendの違いは言葉で言われてもピンと来ないので実際に試した方が早いのね。
irbで試しましょう。

とりあえずこんなモジュールを用意します。
module A
 def hello
  "hello"
 end
end

そして使ってみます。まずはinclude。
class B
 include A
end

test = B.new
test.hello => #"hello"
B.hello => #"NoMethodError"

次にextend。
class C
 extend A
end

C.hello => #"hello"
test = C.new
test.hello => #"NoMethodError"
test.extend(A)
test.hello => #"hello"
つまりincludeはインスタンスメソッドを差し込むのに対して、extendは特異メソッドを挿入するかたちになります。
だからクラス定義の中でextendすると、クラスの特異メソッド=クラスメソッドになります。