2009年8月29日土曜日

[Rails][CodeReading] Rails::Initializer (28) load_gems

Initializer.process で28番目に呼ばれる load_gems メソッド。
前にも
[Rails][CodeReading] Rails::Initializer (25) load_gems
で呼ばれていて、2度目の呼び出しになる。
プラグインが必要とするgemをロードしているとのこと。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (27) add_gem_load_paths

Initializer.process で27番目に呼ばれる add_gem_load_paths メソッド。

process の最初のほうで1度呼ばれているので、
[Rails][CodeReading] Rails::Initializer (4) add_gem_load_paths
2度目の呼び出しになる。
コメントによると、2度目の呼び出しは、プラグインのためとのこと。


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月28日金曜日

[Rails][CodeReading] Rails::Initializer (26) load_plugins

Initializer.process で26番目に呼ばれる load_plugins メソッド。

def load_plugins
plugin_loader.load_plugins
end

処理はplugin_loader(実体はRails::Plugin::Loader)に委譲されます。
以下プラグインのロード処理を、少し詳しく追ってみます。


# rails-2.3.3/lib/rails/plugin/loader.rb
def load_plugins
plugins.each do |plugin|
plugin.load(initializer)
register_plugin_as_loaded(plugin)
end

configure_engines

ensure_all_registered_plugins_are_loaded!
end

なにはともあれ、2行目にいきなり出てくる plugins の出所を探らねば。

# rails-2.3.3/lib/rails/plugin/loader.rb
# 便宜的に、メソッドの定義順を呼ばれる順に変更
def plugins
@plugins ||= all_plugins.select { |plugin| should_load?(plugin) }.sort { |p1, p2| order_plugins(p1, p2) }
end

def all_plugins
@all_plugins ||= locate_plugins
@all_plugins
end

protected
def locate_plugins
configuration.plugin_locators.map do |locator|
locator.new(initializer).plugins
end.flatten
# TODO: sorting based on config.plugins
end

locate_plugins 2行目の configuration は、Initializerに渡されるConfigurationオブジェクトを参照しています。3行目の initializer はInitializerオブジェクトそのもの。
Configuration.plugin_locators のデフォルト値を確認すると、

# rails-2.3.3/lib/initializer.rb
def default_plugin_locators
locators = []
locators << Plugin::GemLocator if defined? Gem
locators << Plugin::FileSystemLocator
end

Plugin::GemLocator, Plugin::FileSystemLocator が含まれます。どうも、この2つのLocatorオブジェクトのpluginsメソッドが肝らしいので、そっちにとんでみる。

# rails-2.3.3/lib/rails/plugin/locator.rb
class FileSystemLocator < Locator
def plugins
initializer.configuration.plugin_paths.flatten.inject([]) do |plugins, path|
plugins.concat locate_plugins_under(path)
plugins
end.flatten
end
~~ 略 ~~
end

class GemLocator < Locator
def plugins
gem_index = initializer.configuration.gems.inject({}) { |memo, gem| memo.update gem.specification => gem }
specs = gem_index.keys
specs += Gem.loaded_specs.values.select do |spec|
spec.loaded_from && # prune stubs
File.exist?(File.join(spec.full_gem_path, "rails", "init.rb"))
end
specs.compact!

require "rubygems/dependency_list"

deps = Gem::DependencyList.new
deps.add(*specs) unless specs.empty?

deps.dependency_order.collect do |spec|
Rails::GemPlugin.new(spec, gem_index[spec])
end
end
end

GemLocatorのほうは、Gemの詳細を知らないと追えそうにないので、FileSystemLocatorのほうを追いかけてみる。
initializer.configuration.plugin_paths はデフォルトで vendor/plugins だけ含みます。

# rails-2.3.3/lib/initializer.rb
def default_plugin_paths
["#{root_path}/vendor/plugins"]
end


(初期設定で) FileSystemLocator.plugins は、locate_plugins_under に $RAILS_ROOT/vendor/plugins を噛ませた結果を返すことになります。

# rails-2.3.3/lib/rails/plugin/locator.rb
def locate_plugins_under(base_path)
Dir.glob(File.join(base_path, '*')).sort.inject([]) do |plugins, path|
if plugin = create_plugin(path)
plugins << plugin
elsif File.directory?(path)
plugins.concat locate_plugins_under(path)
end
plugins
end
end

おお Dir.glob !! ここで、vendor/plugin の中を(再帰的に)見に行ってることになります。
3行目 create_plugin に vendor/plugin のサブディレクトリが次々渡されて、その評価結果が plugins に入る。create_plugin メソッドは以下。

# rails-2.3.3/lib/rails/plugin/locator.rb
def create_plugin(path)
plugin = Rails::Plugin.new(path)
plugin.valid? ? plugin : nil
end

ここで、Rails::Pluginオブジェクトが生成されています。

# rails-2.3.3/lib/rails/plugin.rb (抜粋)
class Plugin
include Comparable

attr_reader :directory, :name

def initialize(directory)
@directory = directory
@name = File.basename(@directory) rescue nil
@loaded = false
end

def valid?
File.directory?(directory) && (has_app_directory? || has_lib_directory? || has_init_file?)
end

private
def lib_path
File.join(directory, 'lib')
end
def classic_init_path
File.join(directory, 'init.rb')
end
def gem_init_path
File.join(directory, 'rails', 'init.rb')
end
def init_path
File.file?(gem_init_path) ? gem_init_path : classic_init_path
end
def has_app_directory?
File.directory?(File.join(directory, 'app'))
end
def has_lib_directory?
File.directory?(lib_path)
end
def has_init_file?
File.file?(init_path)
end
end

valid? メソッドが true を返すのは
  • (初期化時に渡された)パスがディレクトリである

かつ、以下のどれかをみたす場合。
  • サブディレクトリ app/ 存在する
  • サブディレクトリ lib/ が存在する
  • ファイル rails/init.rb または init.rb が存在する

valid? で篩にかけられて残ったPluginオブジェクトが、メソッド呼び出し元へ次々返されていき、最初の Plugin::Loader.load_plugins に入ります。

###

ここから、(やっと)ロード処理。Plugin::Loader.load_pluginsメソッド再掲。

# rails-2.3.3/lib/rails/plugin/loader.rb
def load_plugins
plugins.each do |plugin|
plugin.load(initializer)
register_plugin_as_loaded(plugin)
end

configure_engines

ensure_all_registered_plugins_are_loaded!
end

Plugin.load が、実際に個々のプラグインのコードを読み込む処理をしています。

# rails-2.3.3/lib/rails/plugin.rb
def load(initializer)
return if loaded?
report_nonexistant_or_empty_plugin! unless valid?
evaluate_init_rb(initializer)
@loaded = true
end

private
def classic_init_path
File.join(directory, 'init.rb')
end
def gem_init_path
File.join(directory, 'rails', 'init.rb')
end
def init_path
File.file?(gem_init_path) ? gem_init_path : classic_init_path
end
def evaluate_init_rb(initializer)
if has_init_file?
silence_warnings do
# Allow plugins to reference the current configuration object
config = initializer.configuration

eval(IO.read(init_path), binding, init_path)
end
end
end


eval(IO.read('init.rb'), binding, 'init.rb') が見つかりました。ノノ

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月27日木曜日

[Rails][CodeReading] Rails::Initializer (25) load_gems

Initializer.process で25番目に呼ばれる load_gems メソッド。

def load_gems
unless $gems_rake_task
@configuration.gems.each { |gem| gem.load }
end
end

Configuration.gemsに含まれるgemspecをすべてロードしています。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (24) check_for_unbuilt_gems

Initializer.process で24番目に呼ばれる check_for_unbuilt_gems メソッド。

def check_for_unbuilt_gems
unbuilt_gems = @configuration.gems.select(&:frozen?).reject(&:built?)
if unbuilt_gems.size > 0
# don't print if the gems:build rake tasks are being run
unless $gems_build_rake_task
abort <<-end_error
The following gems have native components that need to be built
#{unbuilt_gems.map { |gem| "#{gem.name} #{gem.requirement}" } * "\n "}

You're running:
ruby #{Gem.ruby_version} at #{Gem.ruby}
rubygems #{Gem::RubyGemsVersion} at #{Gem.path * ', '}

Run `rake gems:build` to build the unbuilt gems.
end_error
end
end
end

frozen されていて、build されていない gem があったらメッセージを出して終了。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (23) add_support_load_paths

Initializer.process で23番目に呼ばれる add_support_load_paths メソッド。

def add_support_load_paths
end


# 何でしょうか?これ。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (22) initialize_metal

Initializer.process で22番目によばれる initialize_metal メソッド。

def initialize_metal
Rails::Rack::Metal.requested_metals = configuration.metals
Rails::Rack::Metal.metal_paths += plugin_loader.engine_metal_paths

configuration.middleware.insert_before(
:"ActionController::ParamsParser",
Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?)
end

名前からわかるとおり、Metal の初期化をしているらしい。

そもそもMetalを知らないのですが。。。
What's New in Edge Rails: Rails Metal という記事に

So Rails Metal is a way to cut out the fat of the MVC request processing stack (and the all the niceties, too) to get the quickest path to your application logic. This is especially useful when you have a single action that you need to run as quickly as possible and can afford to throw away all the support Rails normally provides in exchange for a very fast response time.
(超意訳)Rails Metal は、重厚なMVC処理(と、その恩恵)をすっとばして、アプリケーションロジックへのショートカットを提供します。できる限り速く動作させたいアクションがあり、通常Railsが提供するサポートと引き換えてでもレスポンスの速さを求めるなら、きっと役に立つでしょう。

とあります。

こう書くらしい。

# app/metal/poller.rb
class Poller < Rails::Rack::Metal
def call(env)
if env["PATH_INFO"] =~ /^\/poller/
[200, {"Content-Type" => "application/json"}, Message.recent.to_json]
else
super
end
end
end

パスにマッチしたところでステータスコードとヘッダとレスポンスボディを並べた配列をreturnしている(なんかコード見ただけでも速そう)。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (21) initialize_framework_views

Initializer.process で21番目に呼ばれる initialize_framework_views メソッド。

def initialize_framework_views
if configuration.frameworks.include?(:action_view)
view_path = ActionView::PathSet.type_cast(configuration.view_path)
ActionMailer::Base.template_root = view_path if configuration.frameworks.include?(:action_mailer) && ActionMailer::Base.view_paths.blank?
ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.blank?
end
end

ActionMailer::Base.template_root と、ActionController::Base.view_paths に Configuration.view_path をセットしています(それぞれ、Configuration.frameworksに含まれていれば)。
Configuration.view_path のデフォルトは、おなじみの $RAILS_ROOT/app/views 。

def default_view_path
File.join(root_path, 'app', 'views')
end


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (20) initialize_framework_settings

Initializer.process で20番目に呼ばれる initialize_framework_settings メソッド。

def initialize_framework_settings
configuration.frameworks.each do |framework|
base_class = framework.to_s.camelize.constantize.const_get("Base")

configuration.send(framework).each do |setting, value|
base_class.send("#{setting}=", value)
end
end
configuration.active_support.each do |setting, value|
ActiveSupport.send("#{setting}=", value)
end
end

Configuration.frameworks に含まれるモジュールのBaseクラスに対して、environment.rb に書かれた設定を適用しています。
Configuration.framesorks はフレームワークモジュールを表すシンボル(:active_record のような)を含む配列なのですが、シンボルからBaseクラスを一気に取得しているのが3行目。Rails(ActiveSupport)のクラス検索の特色がよく出ていて面白いので、分解して追ってみました。
$ script/console 
>> :active_record.to_s
=> "active_record"
>> :active_record.to_s.camelize
=> "ActiveRecord"
>> :active_record.to_s.camelize.constantize
=> ActiveRecord
>> :active_record.to_s.camelize.constantize.const_get("Base")
=> ActiveRecord::Base


Configuration.framework という変数に、個々のモジュールに対する設定が格納されているので、sendメソッドで呼び出してBaseクラスに(これもsendメソッドで)渡している。リフレクション(イントロスペクション?)が強力。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月26日水曜日

[Rails][CodeReading] Rails::Initializer (19) initialize_i18n

Initializer.process で19番目に呼ばれる initialize_i18n メソッド。

def initialize_i18n
configuration.i18n.each do |setting, value|
if setting == :load_path
I18n.load_path += value
else
I18n.send("#{setting}=", value)
end
end
end


Configuration.i18n は、Arrayを拡張したOrderedOprionsクラスのオブジェクトが格納されています。

def default_i18n
i18n = Rails::OrderedOptions.new
i18n.load_path = []

if File.exist?(File.join(RAILS_ROOT, 'config', 'locales'))
i18n.load_path << Dir[File.join(RAILS_ROOT, 'config', 'locales', '*.{rb,yml}')]
i18n.load_path.flatten!
end

i18n
end


class Rails::OrderedOptions < Array #:nodoc:
def []=(key, value)
key = key.to_sym

if pair = find_pair(key)
pair.pop
pair << value
else
self << [key, value]
end
end

def [](key)
pair = find_pair(key.to_sym)
pair ? pair.last : nil
end

def method_missing(name, *args)
if name.to_s =~ /(.*)=$/
self[$1.to_sym] = args.first
else
self[name]
end
end

private
def find_pair(key)
self.each { |i| return i if i.first == key }
return false
end
end

デフォルトでは、$RAILS_ROOT/config/locale/*.{rb, yml} ファイルが、I18n.load_path に追加されます。初期状態では、このディレクトリには en.yml というファイルだけが入っていました。environment.rb 内で
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')]
# config.i18n.default_locale = :de
をコメントアウトして適宜書き換えることで、独自のロケールファイルパスを追加したり、デフォルトロケールを変更することができるはず。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (18) initialize_time_zone

Initializer.process で18番目に呼ばれる initialize_time_zone メソッド。

def initialize_time_zone
if configuration.time_zone
zone_default = Time.__send__(:get_zone, configuration.time_zone)

unless zone_default
raise \
'Value assigned to config.time_zone not recognized.' +
'Run "rake -D time" for a list of tasks for finding appropriate time zone names.'
end

Time.zone_default = zone_default

if configuration.frameworks.include?(:active_record)
ActiveRecord::Base.time_zone_aware_attributes = true
ActiveRecord::Base.default_timezone = :utc
end
end
end

タイムゾーンを設定しています。Time.__send__ というメソッドは、Rails で拡張されたメソッドで、ActiveSupport::TimeZoneオブジェクトを返します。

>> Time.__send__(:get_zone,'UTC')
=> #<ActiveSupport::TimeZone:0x8506f60 @name="UTC", @utc_offset=0, @tzinfo=nil>
>>


Configuration.time_zone にデフォルト値はありませんが、config/environment.rb で
config.time_zone = 'UTC'
という記述があります。console から Time.zone_default を参照すると
>> Time.zone_default
=> #<ActiveSupport::TimeZone:0x84cf894 @name="UTC", @utc_offset=0, @tzinfo=nil>


TimeZoneにどんな種類があるのか、コードを見てみると

# activesupport-2.3.3/lib/active_support/values/time_zone.rb
MAPPING = {
"International Date Line West" => "Pacific/Midway",
"Midway Island" => "Pacific/Midway",
"Samoa" => "Pacific/Pago_Pago",
"Hawaii" => "Pacific/Honolulu",
"Alaska" => "America/Juneau",
"Pacific Time (US & Canada)" => "America/Los_Angeles",
"Tijuana" => "America/Tijuana",
"Mountain Time (US & Canada)" => "America/Denver",
"Arizona" => "America/Phoenix",
~~ 略 ~~
"Seoul" => "Asia/Seoul",
"Osaka" => "Asia/Tokyo",
"Sapporo" => "Asia/Tokyo",
"Tokyo" => "Asia/Tokyo",
~~ 略 ~~
"Auckland" => "Pacific/Auckland",
"Wellington" => "Pacific/Auckland",
"Nuku'alofa" => "Pacific/Tongatapu"
}.each { |name, zone| name.freeze; zone.freeze }
MAPPING.freeze

と、全部で142個定義されていました。
environment.rb を
config.time_zone = 'Tokyo'
と書き換えると(もちろん)Time.zone_defaultが変わります。
>> Time.zone_default
=> #<ActiveSupport::TimeZone:0x84fd730 @name="Tokyo", @utc_offset=32400, @tzinfo=nil>


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月25日火曜日

[Rails][CodeReading] Rails::Initializer (17) initialize_whiny_nils

Initializer.process で17番目に呼ばれる initialize_whiny_nils メソッド。

def initialize_whiny_nils
require('active_support/whiny_nil') if configuration.whiny_nils
end

Configuration.whiny_nils が true なら active_support/whiny_nil を require します。
Configuration.whiny_nils のデフォルトはfalse。

def default_whiny_nils
false
end


development環境やtest環境の場合、trueに設定し直される。

# config/environments/development.rb, config/environments/test.rb
# Log error messages when you accidentally call methods on nil.
config.whiny_nils = true


WhinyNil とは

Rubyは動的型付け言語なので、予想外のnilに遭遇したとき、その変数に入るべき(と開発者が考えていた)型を探し当てるために、コードをさまようことになりかねません。
WhinyNilの目的は、

予想外のnil値をできるだけ早期に補足し、nilが検出されたときに開発者により意味のあるエラーメッセージを提供すること

と『実践Rails』にあります。

どういうことかというと、試してみるとすぐわかります。

Ruby添付のirbの場合。
$ irb
irb(main):001:0> nil.size
NoMethodError: undefined method `size' for nil:NilClass
from (irb):1
from /usr/lib/ruby/ruby-1.9.1/bin/irb:12:in `<main>'
irb(main):002:0> nil.save
NoMethodError: undefined method `save' for nil:NilClass
from (irb):2
from /usr/lib/ruby/ruby-1.9.1/bin/irb:12:in `<main>'


Rails console (development環境)の場合。
$ script/console 
>> nil.size
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.size
from (irb):3
from /usr/lib/ruby/ruby-1.9.1/bin/irb:12:in `<main>'
>> nil.save
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.save
from (irb):4
from /usr/lib/ruby/ruby-1.9.1/bin/irb:12:in `<main>'

Arrayじゃないの?とかActiveRecordじゃないの?とか言ってきます。

WhinyNil の実体は、次のような高々30行程度のコードです。

# activesupport-2.3.3/lib/active_support/whiny_nil.rb
class NilClass
WHINERS = [::Array]
WHINERS << ::ActiveRecord::Base if defined? ::ActiveRecord

METHOD_CLASS_MAP = Hash.new

WHINERS.each do |klass|
methods = klass.public_instance_methods - public_instance_methods
class_name = klass.name
methods.each { |method| METHOD_CLASS_MAP[method.to_sym] = class_name }
end

# Raises a RuntimeError when you attempt to call +id+ on +nil+.
def id
raise RuntimeError, "Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id", caller
end

private
def method_missing(method, *args, &block)
raise_nil_warning_for METHOD_CLASS_MAP[method], method, caller
end

# Raises a NoMethodError when you attempt to call a method on +nil+.
def raise_nil_warning_for(class_name = nil, selector = nil, with_caller = nil)
message = "You have a nil object when you didn't expect it!"
message << "\nYou might have expected an instance of #{class_name}." if class_name
message << "\nThe error occurred while evaluating nil.#{selector}" if selector

raise NoMethodError, message, with_caller || caller
end
end


# でもなぜ Array と ActiveRecord だけなんでしょうか。。。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (16) initialize_dependency_mechanism

Initializer.process で16番目に呼ばれる initialize_dependency_mechanism メソッド。

def initialize_dependency_mechanism
ActiveSupport::Dependencies.mechanism = configuration.cache_classes ? :require : :load
end

Configuration.cache_classes がtrueなら :require, falseなら :load が選択されるようです。Configuration.cache_classes のデフォルトはtrueです。

def default_cache_classes
true
end



load と require の違い
リファレンスマニュアルより。

require は同じファイルは一度だけしかロードしませんが、 load は無条件にロードします。また、require は拡張子 .rb や .so を自動的に補完しますが、load は行いません。 require はライブラリのロード、load は設定ファイルの読み込みなどに使うのが典型的な用途です。


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (15) initialize_framework_logging

Initializer.process で15番目に呼ばれる initialize_framework_logging メソッド。

def initialize_framework_logging
for framework in ([ :active_record, :action_controller, :action_mailer ] & configuration.frameworks)
framework.to_s.camelize.constantize.const_get("Base").logger ||= Rails.logger
end

ActiveSupport::Dependencies.logger ||= Rails.logger
Rails.cache.logger ||= Rails.logger
end

ActiveRecord, ActionController, ActionMailer (のうちの必要なもの), ActiveSupport::Dependencies, Rails.cache (RAILS_CACHE) のロガーとして Rails.logger を設定しています。Rails.logger は次のメソッド。

def logger
if defined?(RAILS_DEFAULT_LOGGER)
RAILS_DEFAULT_LOGGER
else
nil
end
end


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (14) initialize_logger

Initializer.process で14番目に呼ばれる initialize_logger メソッド。

def initialize_logger
# if the environment has explicitly defined a logger, use it
return if Rails.logger

unless logger = configuration.logger
begin
logger = ActiveSupport::BufferedLogger.new(configuration.log_path)
logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase)
if configuration.environment == "production"
logger.auto_flushing = false
end
rescue StandardError => e
logger = ActiveSupport::BufferedLogger.new(STDERR)
logger.level = ActiveSupport::BufferedLogger::WARN
logger.warn(
"Rails Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " +
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
)
end
end

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }
end


最初に、Rails.logger を見て、nil でなければ何もせず返ります。Rails.logger は

def logger
if defined?(RAILS_DEFAULT_LOGGER)
RAILS_DEFAULT_LOGGER
else
nil
end
end

となっているので、RAILS_DEFAULT_LOGGER が設定されていれば、何もせず返ることになります。

unless logger = configuration.logger
の行まできたとき、logger は必ずnilなので、この = が成り立つのは、configuration.logger が nil のとき。
したがって、logger = nil, configuration.logger != nil のときに unless の中が実行されます。
logger の実体はActiveSupport::BufferedLoggerクラスのインスタンスで、初期化時にログファイルのパスを渡しています(configuration.log_path)。
Configuratoin.log_path のデフォルト値は以下のメソッドで与えられます。

def default_log_path
File.join(root_path, 'log', "#{environment}.log")
end

developmentモードの場合、ログファイルは $RAILS_ROOT/log/development.log となります。

初期化後すぐにログレベル(logger.level)の設定も行っています。Configuration.log_level のデフォルト設定メソッドが

def default_log_level
environment == 'production' ? :info : :debug
end

なので、production環境の場合は INFO, それ以外は DEBUG レベルに設定されるようです。

また、ログファイルが見つからない場合 or 書き込み権限がない場合は、エラーを拾って出力先を STDERR に変更しています。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (13) initialize_framework_caches

Initializer.process で13番目に呼ばれる initialize_framework_caches メソッド。

def initialize_framework_caches
if configuration.frameworks.include?(:action_controller)
ActionController::Base.cache_store ||= RAILS_CACHE
end
end

コードに書いてある通り、なにも付け加えることはありません。
ActionControllerを使用する場合は、chache_store に RAILS_CACHE (デフォルトはActiveSupport::Cache::MomoryStore) をセットしています。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月24日月曜日

[Rails][CodeReading] Rails::Initializer (12) initialize_cache

Initializer.process で12番目に呼ばれる initialize_cache メソッド。

def initialize_cache
unless defined?(RAILS_CACHE)
silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(configuration.cache_store) }

if RAILS_CACHE.respond_to?(:middleware)
# Insert middleware to setup and teardown local cache for each request
configuration.middleware.insert_after(:"ActionController::Failsafe", RAILS_CACHE.middleware)
end
end
end


メソッド名からしてキャッシュの初期化をしているのでしょうが、歯が立たず。
RAILS_CACHE.respond_to? という文からわかるように、RAILS_CACHEという変数にはクラスオブジェクトが入るはず。consoleでみてみると、
$ script/console 
>> p RAILS_CACHE
#<ActiveSupport::Cache::MemoryStore:0x890a8b4 @data={}>

と出ました。デフォルトではActiveSupport::Cache::MomoryStoreを使うらしい。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (11) initialize_database

Initializer.process で11番目に呼ばれる initialize_database メソッド。

def initialize_database
if configuration.frameworks.include?(:active_record)
ActiveRecord::Base.configurations = configuration.database_configuration
ActiveRecord::Base.establish_connection
end
end

Configuration.database_configuration は、config/database.yml を評価して返す。

def default_database_configuration_file
File.join(root_path, 'config', 'database.yml')
end

def database_configuration
require 'erb'
YAML::load(ERB.new(IO.read(database_configuration_file)).result)
end

ActiveRecord::Base.establish_connection は、ドキュメントによると
Establishes the connection to the database. Accepts a hash as input where the +:adapter+ key must be specified with the name of a database adapter (in lower-case) example for regular databases (MySQL, Postgresql, etc):
(データベースコネクションを確立する。引数に1つのハッシュを取る。引数のハッシュは、:adapterキーに紐付けられたデータベースアダプタ名(MySQL, Postgresql, など)を含んでいなければならない。)

とのこと。
# 初期化の時点でデータベースにつなぎに行くということ??

ActiveRecord::Base.establish_connection の実体は、activerecordの奥深くにありました。


# activerecord-2.3.3/lib/active_record/connection_adapters/abstract_adapter.rb
def self.establish_connection(spec = nil)
case spec
when nil
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
@@connection_handler.establish_connection(name, spec)
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
else
raise AdapterNotSpecified, "#{spec} database is not configured"
end
else
spec = spec.symbolize_keys
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end

begin
require 'rubygems'
gem "activerecord-#{spec[:adapter]}-adapter"
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
begin
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
rescue LoadError
raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
end
end

adapter_method = "#{spec[:adapter]}_connection"
if !respond_to?(adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
end

remove_connection
establish_connection(ConnectionSpecification.new(spec, adapter_method))
end
end

たしかに、:adapterキーがなければ例外を投げている(spec.key?(:adapter) then raise AdapterNotSpecified)。
# このあたり、いつかきちんと読みたい。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (10) initialize_encoding

Initializer.process で10番目に呼ばれる initialize_encoding メソッド。

def initialize_encoding
$KCODE='u' if RUBY_VERSION < '1.9'
end

1.9以前のバージョン(実質1.8)の場合に、$KCODE を 'u' (utf-8)に設定しています。
# 1.9から$KCODEは廃止されたので。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月23日日曜日

[Rails][CodeReading] Rails::Initializer (9) preload_frameworks

Initializer.process で9番目に呼ばれる preload_frameworks メソッド。

# Preload all frameworks specified by the Configuration#frameworks.
# Used by Passenger to ensure everything's loaded before forking and
# to avoid autoload race conditions in JRuby.
def preload_frameworks
if configuration.preload_frameworks
configuration.frameworks.each do |framework|
# String#classify and #constantize aren't available yet.
toplevel = Object.const_get(framework.to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
toplevel.load_all! if toplevel.respond_to?(:load_all!)
end
end
end

詳細不明・・・(ごめんなさい)。
Configuration.preload_frameworks は、デフォルトで false になっています。

def default_preload_frameworks
false
end


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (8) load_environment

Initializer.process で8番目に呼ばれる load_environment メソッド。

def load_environment
silence_warnings do
return if @environment_loaded
@environment_loaded = true

config = configuration
constants = self.class.constants

eval(IO.read(configuration.environment_path), binding, configuration.environment_path)

(self.class.constants - constants).each do |const|
Object.const_set(const, self.class.const_get(const))
end
end
end

Configuration.environment_path を eval しています。Configuration.environment_path は

def environment_path
"#{root_path}/config/environments/#{environment}.rb"
end
def environment
::RAILS_ENV
end

なので、config/envirionments 以下の、RAILS_ENV で指定された実行モード設定ファイル(デフォルトはdevelopment.rb)を読み込むことになります。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (7) add_plugin_load_paths

Initializer.process で7番目に呼ばれる add_plugin_load_paths メソッド。

def add_plugin_load_paths
plugin_loader.add_plugin_load_paths
end

plugin_loader (実体はRails::Plugin::Loader) に委譲されているので、そちらをみてみます。

# rails-2.3.3/lib/rails/plugin/loader.rb
def add_plugin_load_paths
plugins.each do |plugin|
plugin.load_paths.each do |path|
$LOAD_PATH.insert(application_lib_index + 1, path)

ActiveSupport::Dependencies.load_paths << path

unless configuration.reload_plugins?
ActiveSupport::Dependencies.load_once_paths << path
end
end
end

$LOAD_PATH.uniq!
end

plugins というのは Rails::Plugin の配列で、Plugin.load_paths というのが $LOAD_PATH や ActiveSupport::Dependencies.load_paths に追加されています。その Plugin.load_paths の正体がこれ。

# rails-2.3.3/lib/rails/plugin.rb
module Rails
class Plugin
~~ 略 ~~
def initialize(directory)
@directory = directory
@name = File.basename(@directory) rescue nil
@loaded = false
end
~~ 略 ~~
def load_paths
report_nonexistant_or_empty_plugin! unless valid?

returning [] do |load_paths|
load_paths << lib_path if has_lib_directory?
load_paths << app_paths if has_app_directory?
end.flatten
end
~~ 略 ~~
private
def app_paths
[ File.join(directory, 'app', 'models'), File.join(directory, 'app', 'helpers'), controller_path, metal_path ]
end

def lib_path
File.join(directory, 'lib')
end
end
end


[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (6) set_autoload_paths

Initializer.process で6番目に呼ばれる set_autoload_paths メソッド。

def set_autoload_paths
ActiveSupport::Dependencies.load_paths = configuration.load_paths.uniq
ActiveSupport::Dependencies.load_once_paths = configuration.load_once_paths.uniq

extra = ActiveSupport::Dependencies.load_once_paths - ActiveSupport::Dependencies.load_paths
unless extra.empty?
abort <<-end_error
load_once_paths must be a subset of the load_paths.
Extra items in load_once_paths: #{extra * ','}
end_error
end

# Freeze the arrays so future modifications will fail rather than do nothing mysteriously
configuration.load_once_paths.freeze
end


Configuration.load_paths を、ActiveSupport::Dependencies.load_paths に、
Configuration.load_once_paths を、ActiveSupport::Dependencies.load_once_paths に
設定しています。

この ActiveSupport::Dependencies.load_paths がどう使われるかというと。

未知の定数が出てきたときに呼ばれる ActiveSupport::Dependencies.load_missing_constant の中で

# activesupport/lib/dependencies.rb
# Load the constant named +const_name+ which is missing from +from_mod+. If
# it is not possible to load the constant into from_mod, try its parent module
# using const_missing.
def load_missing_constant(from_mod, const_name)
~~ 略 ~~
qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore
name_error = NameError.new("uninitialized constant #{qualified_name}")

file_path = search_for_file(path_suffix)
~~ 略 ~~
end

と定数->ファイルパス(のsuffix)への変換が行われ(qualified_name.underscore)、search_for_file メソッドへとファイルパスが引き渡されるのですが、search_for_file で

# activesupport/lib/active_support/dependencies.rb
# Search for a file in load_paths matching the provided suffix.
def search_for_file(path_suffix)
path_suffix = path_suffix + '.rb' unless path_suffix.ends_with? '.rb'
load_paths.each do |root|
path = File.join(root, path_suffix)
return path if File.file? path
end
nil # Gee, I sure wish we had first_match ;-)
end

と load_paths と path_suffix をくっつけて(そのファイルが存在すれば)ロードすべきファイルのパスを返す、という仕組み。
この仕組みの詳細は『実践Rails』に詳しいです。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (5) require_frameworks

Initializer.process で5番目に呼ばれる require_frameworks メソッド。

def require_frameworks
configuration.frameworks.each { |framework| require(framework.to_s) }
rescue LoadError => e
# Re-raise as RuntimeError because Mongrel would swallow LoadError.
raise e.to_s
end


説明不要。Configuration.frameworks に含まれるモジュールをrequireしています。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月22日土曜日

[Rails][CodeReading] Rails::Initializer (4) add_gem_load_paths

Initializer.process で4つめに呼ばれる add_gem_load_paths メソッド。

def add_gem_load_paths
Rails::GemDependency.add_frozen_gem_path
unless @configuration.gems.empty?
require "rubygems"
@configuration.gems.each { |gem| gem.add_load_paths }
end
end

Configuration.gems は、デフォルトでは空です。

def default_gems
[]
end


Configuration に gem というメソッドがあり、

def gem(name, options = {})
@gems << Rails::GemDependency.new(name, options)
end

environment.rb に、

# Specify gems that this application depends on and have them installed with rake gems:install
# config.gem "bj"

という説明があるので、 config.gem を書いておくと Configuration.gems に Rails::GemDependency が追加される仕組みになっているようです。

GemDependency.add_load_paths はどうなっているかというと

class GemDependency < Gem::Dependency
~~ 略 ~~
def add_load_paths
self.class.add_frozen_gem_path
return if @loaded || @load_paths_added
if framework_gem?
@load_paths_added = @loaded = @frozen = true
return
end
gem self
@spec = Gem.loaded_specs[name]
@frozen = @spec.loaded_from.include?(self.class.unpacked_path) if @spec
@load_paths_added = true
rescue Gem::LoadError
end
end


たぶん、8行目あたりの gem self がポイント。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (3) set_load_path

Initializer.process で3つめに呼ばれる set_load_path メソッド。名前から、重要メソッドと推測できます。

def set_load_path
load_paths = configuration.load_paths + configuration.framework_paths
load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
$LOAD_PATH.uniq!
end

Rails::Configuration.load_paths と Rails::Configuration.framework_paths を、Ruby組み込み変数 $LOAD_PATH に追加しています。

Configuration.load_paths, はConfigurationのインスタンス変数で、 Configuration初期化時にデフォルトにセットされます。

def initialize
~~ 略 ~~
self.load_paths = default_load_paths
~~ 略 ~~
end


def default_load_paths
paths = []

# Add the old mock paths only if the directories exists
paths.concat(Dir["#{root_path}/test/mocks/#{environment}"]) if File.exists?("#{root_path}/test/mocks/#{environment}")

# Add the app's controller directory
paths.concat(Dir["#{root_path}/app/controllers/"])

# Followed by the standard includes.
paths.concat %w(
app
app/metal
app/models
app/controllers
app/helpers
app/services
lib
vendor
).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) }

paths.concat builtin_directories
end

def builtin_directories
# Include builtins only in the development environment.
(environment == 'development') ? Dir["#{RAILTIES_PATH}/builtin/*/"] : []
end


ここで、アプリの app/ 以下がロードパスに追加されています。
# app/controllers が 重複して追加されてる??
また、development環境の場合は、最後に rails/builtin がパスに追加されます。

processメソッドにブロックが与えられた場合(config/environment.rbから呼ばれたとき)は、load_path が environment.rb で指定した値で上書きされているはずです。

Configuration.framework_paths のほうは、変数ではなくメソッドになっています。

def framework_paths
paths = %w(railties railties/lib activesupport/lib)
paths << 'actionpack/lib' if frameworks.include?(:action_controller) || frameworks.include?(:action_view)

[:active_record, :action_mailer, :active_resource, :action_web_service].each do |framework|
paths << "#{framework.to_s.gsub('_', '')}/lib" if frameworks.include?(framework)
end

paths.map { |dir| "#{framework_root_path}/#{dir}" }.select { |dir| File.directory?(dir) }
end

ここで railties, activesupport, actionpack, activerecord, actionmailer, activeresource, actionwebservice と、すべてのRailsフレームワークがロードパスに追加されます。frameworks変数でロードするモジュールをフィルタしているので、そのデフォルト値をみてみると

def default_frameworks
[ :active_record, :action_controller, :action_view, :action_mailer, :active_resource ]
end

となっています。

なお、environment.rb で config.frameworks を編集して、

config.frameworks -= [ :active_record ]

たとえばこのように activerecordモジュールを frameworks から除いても、activerecord がロードパスから外れることはありません。config/boot.rb 内で、一度デフォルトのConfigurationで set_load_path が実行され、そのときにロードパスに追加されるためです。
(ただし、ロードはされません。当然か ^^;)

↑の設定で script/console を実行すると、
>> p ActiveRecord  # 例外発生
NameError: uninitialized constant ActiveRecord
from /usr/lib/ruby/ruby-1.9.1/lib/ruby/gems/1.9.1/gems/activesupport-2.3.3/lib/active_support/dependencies.rb:443:in `load_missing_constant'
from /usr/lib/ruby/ruby-1.9.1/lib/ruby/gems/1.9.1/gems/activesupport-2.3.3/lib/active_support/dependencies.rb:80:in `const_missing_with_dependencies'
from /usr/lib/ruby/ruby-1.9.1/lib/ruby/gems/1.9.1/gems/activesupport-2.3.3/lib/active_support/dependencies.rb:92:in `const_missing'
from (irb):1
from /usr/lib/ruby/ruby-1.9.1/bin/irb:12:in `<main>'
>> require 'active_record' # activerecord をロード(成功)
=> ["ActiveRecord"]
>> p ActiveRecord # 今度は見つかる
ActiveRecord
=> ActiveRecord


という状態。


set_load_path で $LOAD_PATH にパスを追加するときのコードを見ると、Railsでのパスの検索順がみえてきます。

load_paths = configuration.load_paths + configuration.framework_paths
load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }

reverse して $LOAD_PATH に unshift しているので、最初に検索されるのはアプリの app ディレクトリ(のはず)。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (2) install_gem_spec_stubs

Initializer.process 内で 2つめに呼ばれる install_gem_spec_stubs メソッド。

def install_gem_spec_stubs
unless Rails.respond_to?(:vendor_rails?)
abort %{Your config/boot.rb is outdated: Run "rake rails:update".}
end

if Rails.vendor_rails?
begin; require "rubygems"; rescue LoadError; return; end

stubs = %w(rails activesupport activerecord actionpack actionmailer activeresource)
stubs.reject! { |s| Gem.loaded_specs.key?(s) }

stubs.each do |stub|
Gem.loaded_specs[stub] = Gem::Specification.new do |s|
s.name = stub
s.version = Rails::VERSION::STRING
s.loaded_from = ""
end
end
end
end


Vendor Rails の場合に、rails, activesupport, activerecord, actionpack, actionmailer, activeresource の stub gem を用意しているようです。gemの仕組みを知らないので詳細はわからない...。

コメントには、
This allows Gem plugins to depend on Rails even when the Gem version of Rails shouldn't be loaded.
とあります。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

[Rails][CodeReading] Rails::Initializer (1) check_ruby_version

processメソッドの中で最初に呼ばれるメソッド check_ruby_version です。

def check_ruby_version
require 'ruby_version_check'
end


実体は、ruby_version_check.rb にあります。

# rails/lib/ruby_version_check.rb
min_release = "1.8.2 (2004-12-25)"
ruby_release = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
if ruby_release =~ /1\.8\.3/
abort <<-end_message

Rails does not work with Ruby version 1.8.3.
Please upgrade to version 1.8.4 or downgrade to 1.8.2.

end_message
elsif ruby_release < min_release
abort <<-end_message

Rails requires Ruby version #{min_release} or later.
You're running #{ruby_release}; please upgrade to continue.

end_message
end

Rails 2.3.3 のサポートするRubyは 1.8.2 (2004-12-25) 以降(1.8.3除く)であることがわかります。対象外バージョンのRubyで実行された場合は例外がスローされます。

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

2009年8月20日木曜日

[Rails][CodeReading] Railsの初期化コードを読む (イントロ&目次)

実践Rails』という本を読んでいます。Ruby/Railsの中身(実装)まで踏み込んで書かれていて、読みごたえあります。

その中の1節
「2.5.2 アプリケーションを初期化するための20の手順」
をたよりに、Railsの初期化コード(rails-version/lib/initializer.rb)を読んでみようという(ありがち王道な)試みです。

Rails のバージョンは 2.3.3です。

前置き

initializer.rb の前に、(一番初めから追ってみたかったので...)まずはエントリポイントとなる script/server を。
# script/server
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require 'commands/server'

script/serverでは、2つのファイルを require しているだけです。
1つずつ見ていきます。まずは、config/boot.rb。
# config/boot.rb (抜粋)
# Don't change this file!
# Configure your app in config/environment.rb and config/environments/*.rb

RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)

module Rails
class << self
def boot!
unless booted?
preinitialize
pick_boot.run
end
end

def pick_boot
(vendor_rails? ? VendorBoot : GemBoot).new
end
end

class Boot
def run
load_initializer
Rails::Initializer.run(:set_load_path)
end
end

class VendorBoot < Boot
def load_initializer
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
Rails::Initializer.run(:install_gem_spec_stubs)
Rails::GemDependency.add_frozen_gem_path
end
end

class GemBoot < Boot
def load_initializer
self.class.load_rubygems
load_rails_gem
require 'initializer'
end
end
end

# All that for this:
Rails.boot!

boot.rb は

  • グローバル変数 RAILS_ROOT をセット

  • Railsモジュールにboot!メソッドを追加

  • Rails.boot! の呼び出し


という流れになっています。
肝となる Rails.boot! メソッドでは、pick_boot メソッドで Bootクラスインスタンスが生成され(*1)、Boot.runが呼ばれます。そして、このBoot.runの中でRails::Initializer.runが呼ばれる、という仕組み。

(*1) 実際に生成されるのはBootクラスのサブクラスであるVenderBootクラスもしくはGemBootクラスのインスタンス。vender/railsが存在する場合はVenderBoot, なければGemBootが選択される。

次に、script/server内で2つめにrequireされるcommands/server.rb。こちらはRails本体の中にあります。


# rails/commands/server.rb
~~略~~
# 71行目から
if File.exist?(options[:config])
config = options[:config]
if config =~ /\.ru$/
cfgfile = File.read(config)
if cfgfile[/^#\\(.*)/]
opts.parse!($1.split(/\s+/))
end
inner_app = eval("Rack::Builder.new {( " + cfgfile + "\n )}.to_app", nil, config)
else
require config
inner_app = Object.const_get(File.basename(config, '.rb').capitalize)
end
else
require RAILS_ROOT + "/config/environment"
inner_app = ActionController::Dispatcher.new
end
~~略~~

-c または --config オプションを指定しなければ、デフォルト環境設定ファイルの config/environment.rb がrequireされます。(ここにいたのか environment.rb)
そして、config/environment.rb の中で

# config/environment.rb
RAILS_GEM_VERSION = '2.3.3' unless defined? RAILS_GEM_VERSION

# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')

Rails::Initializer.run do |config|
# Settings in config/environments/* take precedence over those specified here.
~~略~~
end

Rails::Initializer.runが再び(今度は引数なし/ブロック付きで)呼ばれます。
# Rails::Initializer は初期化処理で2回呼ばれる・・・?


Rails::Initializer

前置きはこのぐらいにして、Rails::Initializer のコードをみてみます。
コメントとか、いろいろはしょって骨格だけ。


# rails/lib/initializer.rb
require 'logger'
require 'set'
require 'pathname'

$LOAD_PATH.unshift File.dirname(__FILE__)
require 'railties_path'
require 'rails/version'
require 'rails/plugin/locator'
require 'rails/plugin/loader'
require 'rails/gem_dependency'
require 'rails/rack'


RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV)

module Rails
class Initializer
def self.run(command = :process, configuration = Configuration.new)
yield configuration if block_given?
initializer = new configuration
initializer.send(command)
initializer
end

def process
Rails.configuration = configuration

check_ruby_version # (1)
install_gem_spec_stubs # (2)
set_load_path # (3)
add_gem_load_paths # (4)
require_frameworks # (5)
set_autoload_paths # (6)
add_plugin_load_paths # (7)
load_environment # (8)
preload_frameworks # (9)
initialize_encoding # (10)
initialize_database # (11)
initialize_cache # (12)
initialize_framework_caches # (13)
initialize_logger # (14)
initialize_framework_logging # (15)
initialize_dependency_mechanism # (16)
initialize_whiny_nils # (17)
initialize_time_zone # (18)
initialize_i18n # (19)
initialize_framework_settings # (20)
initialize_framework_views # (21)
initialize_metal # (22)
add_support_load_paths # (23)
check_for_unbuilt_gems # (24)
load_gems # (25)
load_plugins # (26)
add_gem_load_paths # (27)
load_gems # (28)
check_gem_dependencies # (29)
return unless gems_dependencies_loaded # (30)
load_application_initializers # (31)
after_initialize # (32)
initialize_database_middleware # (33)
prepare_dispatcher # (34)
initialize_routing # (35)
load_observers # (36)
load_view_paths # (37)
load_application_classes # (38)
disable_dependency_loading # (39)
Rails.initialized = true
end

~~略~~

def set_load_path
load_paths = configuration.load_paths + configuration.framework_paths
load_paths.reverse_each { |dir| $LOAD_PATH.unshift(dir) if File.directory?(dir) }
$LOAD_PATH.uniq!
end

~~略~~
end
end


(runメソッドだけ取り出してみます。)

def self.run(command = :process, configuration = Configuration.new)
yield configuration if block_given?
initializer = new configuration
initializer.send(command)
initializer
end

(1行目) メソッドシグネチャ。Initializer.runは引数を2つ(command, configuration)とります。Configurationは、同じRailsモジュール内で定義されているクラス。
(2行目) ブロックを与えると、configurationがyieldされます。
(3行目) Initializerがnewされます。いきなり new とあるのは、self.new の略記らしい。普段Javaを使っていると、new Configuration()に見えてまぎらわしい。(--;;
(4行目) commandで与えられたメソッドを呼び出します。
(5行目) Initializerをreturn(?)

前置きでみた2つのInitializer.runについて見直すと、

(1)
config/boot.rb 内での Initializer.run呼び出しは

Rails::Initializer.run(:set_load_path)

とcommand引数が与えられているため4行目は set_load_path メソッドの呼び出しとなります。

(2)
config/environment.rb 内での Initializer.run呼び出しは

Rails::Initializer.run do |config|
# settings
end

で、command引数が省略されているため4行目は processメソッド(デフォルト)の呼び出しとなります。ブロックが与えられているため、2行目のyieldも実行されます。

肝となるのは、(2)のprocessメソッドを呼び出しているほうになります。

processメソッドはすごくシンプルで、サブメソッドを順々に呼び出しているだけです(呼ばれるメソッド名を眺めるだけでも、なんとなくやっていることがわかる)。


今日はここまで。。。
次から、processメソッドで呼ばれるメソッド (1)~(39) を読んでいきたいと思います。
続く。たぶん。

----

2009年8月12日水曜日

[Rails][Heroku] heroku db:push ではまった

heroku に、ローカルDBのデータをimportするには
$ heroku db:push 

というコマンドを使います。

CSVなどの中間ファイルにいったん落とす必要はありません。

で、これが何度試みても失敗。。。

どうやら、db:push をするのに必要な taps というgemが依存するRailsのバージョンの問題だったらしいです。

taps がv2.2.2のactiverecordに依存していて、私が使っているのがRails 2.3.2(当然、activerecordも2.3.2)。

Railsを2.2.2にバージョンダウンして再度試みるとうまくいきました。
# というと簡単そうですが、どうも Rails 2.2.2 と Ruby 1.9 の相性がわるく、実際は Ruby も 1.9 -> 1.8 にバージョンダウンするはめに...。

アプリ自体は問題なく動いているので、gemに依存しない形でのデータ移行手段がほしいなぁ。

2009年8月11日火曜日

[Rails][習作] 簡易ブックマークアプリ (5)

[Rails][習作] 簡易ブックマークアプリ (4)  の続きです。(もはや自分しかわからない備忘録。)

プラグインを使ってリファクタした箇所が2点ほど。
  • 月別アーカイブメニューの書き直し(自作プラグイン)
  • タグクラウドの書き直し(acts_as_taggable_steroidsプラグイン)
月別アーカイブメニュー/プラグイン導入編

前回作成した、アーカイブメニュー作成支援プラグイン(α--版)を使いました。

Bookmarkモデルに

acts_as_archivable

の一文を追加。

コントローラのアクションメソッドに

@archive_counts = Bookmark.monthly_archive_counts

の一文を追加。

@archive_counts変数は、{ 年 => { 月 => 件数, 月 => 件数, ... }, ... } というハッシュに格納されるので、ビュー側ではこんな感じのアーカイブメニューが(わりと)簡単にできます。


タグクラウド/プラグイン編

acts_as_taggable_on_steroids という高機能タグ管理プラグインを導入。(知らずに自分で作ろうとしていた。。まぁ練習にはいいのだけれど。)

参考: acts_as_taggable_on_steroids のまとめ

script/plugin install で導入して、Bookmarkモデルに

acts_as_taggable

を追加するだけで高機能なタグ管理システムが使えるようになります。

プラグイン導入ついでに、複数タグでのエントリ絞込み機能を追加。次の find 文で、指定したすべてのタグを持つBookmarkモデルが検索されます。

@bookmarks = Bookmark.find_tagged_with(tags, :match_all=>true, :order=>"bookmarks.created_at desc").paginate(:page=>params[:page], :per_page=>5)
ビューではこんな感じで、現在の絞込み状況と「さらに絞る」リンクを表示。



また、新規作成/編集時のタグ付け支援(今ままでにつけたタグを表示&クリックでテキストボックスに自動追加)も実装。

(タグが多い場合の)見やすい表示と、補完・サジェスト機能はコンゴノカダイです。

その他、
  • 新規追加時に、titleフィールドを自動取得(URLが指すHTMLから<title>を切り出す)
  • 同じURLを参照するブックマークを複数個作成できないように修正(「編集」ページにとばす)
  • will_paginateプラグインを入れてページング処理
  • インデックスページのレイアウトをちょっとだけカスタマイズ
などちまちまと改良。

おもちゃなりに、ほんのりと体裁が整ってきたでしょうか。。。

Heroku (Gardenではなく本家のほう)に上げてみました。

http://toybookmark.heroku.com/

(データはlivedoor bookmarkからエクスポートしたもの)

2009年8月8日土曜日

[Rails][プラグイン]acts_asプラグインの作りかた - 超入門 -

ブログなどについている、月別アーカイブ作成支援に汎用モジュールがほしかったので、(勉強も兼ねて)簡単なプラグインを作成しました。
(スキルレベル:知識ゼロ。プラグインてなに?)

少し調べてみたところ、ActiveRecordを拡張する acts_as_xxx という手法(フレームワーク?)が適当そうだったため、その作法に則ります。テーブルやカラムの追加等、あまり複雑なことはせず、ActiveRecord作成日付フィールド(普通は created_at)だけを参照します。

参考にしたのは、↓の記事と acts_as_taggable_on_steroidsプラグインのソース。

プラグイン作成

まずは、pluginをgenerateします。プラグイン名は acts_as_archivable。
$ script/generate plugin acts_as_archivable
generate を実行すると、vendor/plugin 以下に acts_as_archivable ディレクトリ(と中にプラグインの雛形)が作られます。
generateされたファイルのうち、編集したのは以下の2つだけです。
  • init.rb
  • lib/acts_as_archivable.rb
init.rb には次のコードを追加します。

# init.rb
require 'acts_as_archivable'

ActiveRecord::Base.class_eval do
include ActiveRecord::Acts::Archivable
end


init.rb は Railsによりアプリケーション初期化時に読み込まれます(西和則著「Ruby on Rails入門―優しいRailsの育て方」)。ここでは、ActiveRecord::Baseクラスに、ActiveRecord::Acts::Archivable モジュールをMix-inしているだけです。

Archivableモジュールの実体は、lib/acts_as_archivable.rb の中で定義します。

# acts_as_archivable.rb
module ActiveRecord
module Acts
module Archivable
def self.included(base)
base.extend(ActMacro)
end
end

module ActMacro
# acts_as_ メソッド(ActiveRecordクラス内で呼び出す)
# 例外処理、クラスメソッドのインクルード
def acts_as_archivable(col_name = "created_at")
# 指定された作成日カラムがActiveRecordクラスになければ例外を発生させる
raise NoCreatedAtColumnError unless self.column_names.include? col_name
# 指定された作成日カラムの型が:datetimeでなければ例外を発生させる
raise TypeError unless self.columns_hash[col_name].type.equal? :datetime
# クラスメソッドをインクルード
self.extend(ClassMethods)
self.set_created_col_name col_name
end

class NoCreatedAtColumnError < StandardError; end
class TypeError < StandardError; end
end

module ClassMethods
# ActiveRecordと、作成日カラム名を紐付けるマップ
@@class_col_hash = Hash.new
def set_created_col_name col_name
@@class_col_hash.store self.class.name, col_name
end
def get_created_col_name
@@class_col_hash[self.class.name]
end

# 月別アーカイブ作成支援メソッド
# ここでは、月ごとに作成されたActiveRecordの件数をもつハッシュを作成して返している
# ex. {2008=>{5=>10,8=>3,11=>6},2009=>{1=>5,3=>5,...}}
# パフォーマンス/使い易さを考えると、もう少しやりようがあると思うけどとりあえず。。。
def monthly_archive_counts
counts = Hash.new
records = self.find(:all)
records.each do |record|
year = record[get_created_col_name].year
month = record[get_created_col_name].month
if counts[year] == nil
counts.store year, Hash.new(0)
end
counts[year][month] += 1
end
return counts
end
end
end
end


self.includedというのはModuleクラスのメソッドで、リファレンスマニュアルの説明には
self が include されたときに対象のクラスまたはモジュールを引数にインタプリタから呼び出されます。

とあります。

acts_as_archivableが、(プラグインに慣れている人にはたぶんおなじみの)、拡張対象のActiveRecordクラスから呼び出すメソッドになります。
作成日カラムとして"created_at"以外を使う場合に対応できるよう、作成日カラム名を与えるようにしています(デフォルト値は"created_at")。
  • 指定された作成日カラムが存在しない場合
  • 存在するが :datetime タイプでない場合
には、例外を発生させています。
例外処理のあと、ActiveRecordと作成日カラム名の対応づけと、目的となるクラスメソッドのインクルードを行います。

使いかた

準備として

class Bookmark < ActiveRecord::Base
acts_as_archivable
end

のように ActiveRecord クラス内で acts_as_archivable メソッドを呼ぶだけです。これで、Bookmarkクラスに monthly_archive_counts メソッドが追加されます。

bookmarksテーブルに
sqlite> select id, created_at from bookmarks;
5|2008-10-01 09:00:00
6|2008-10-01 09:00:00
7|2008-10-01 09:00:00
8|2008-11-01 09:00:00
9|2009-02-01 09:00:00
10|2009-02-01 09:00:00
11|2009-04-01 09:00:00
12|2009-04-01 09:00:00
13|2009-04-01 09:00:00
14|2009-04-01 09:00:00
16|2009-06-01 09:00:00

というデータが入っているとき、Bookmark.monthly_archive_counts は次のハッシュを返します。

$ script/console
Loading development environment (Rails 2.3.3)
>> counts = Bookmark.monthly_archive_counts
=> {2008=>{10=>3, 11=>1}, 2009=>{2=>2, 4=>4, 6=>1}}
>> counts[2009][4]
=> 4


ビューでの使用イメージはこんな感じ。


ちゃんと使えるプラグインを作ろうとすると、テーブルやカラムを追加したり色々複雑になりますが、acts_asプラグインの骨格としてはこんな感じでしょうか。

よくできてるなー。
Mix-inの強力さを(あらためて)実感。

2009年8月4日火曜日

[Railsのキホン]リクエストパラメータ

知っている人には常識な話ですが、Railsでのリクエストパラメータの扱いについて。

Railsでは、ルーティング情報とクエリパラメータをハッシュマップ(パラメータ情報というらしい)に格納して、コントローラへ渡します。コントローラからは、params という変数でこのマップにアクセスします。
たとえば、"key"というリクエストパラメータを取得するには、 params[:key] と書きます。
※このとき、:key がルーティング定義(config/routes.rb)で使われていてはいけない。

ハッシュマップなので、ひとつのクエリパラメータ名に複数の値を対応させようとして、
http://foo.com/controller/action?key=val1&key=val2

とすると後の値(この場合 "val2" で params[:key] が上書きされてしまいます。
(仕組みを知らないとあわてる。。)

少しきちんとRailsのリクエストパラメータの受け渡しについて調べてみました。

参考:Railsを使ってURLパラメータの引き渡しと読み込みを行う方法

Railsではクエリパラメータのキーに、以下の3種類のデータ型をバインドすることができます。
  1. 文字列
  2. 配列
  3. ハッシュマップ

1. 文字列

ひとつのパラメータキーにひとつの値が対応する、基本形。
http://foo.com/params/show_param?key1=val1&key2=val2
とします。
パラメータ情報(params)は
{"key1"=>"val1", "amp"=>nil, "key2"=>"val2", "controller"=>"params", "action"=>"show_param"}

となる。

2. 配列
ひとつのパラメータキーに、複数の値を対応させたい場合の書式。
http://foo.com/params/show_param?key[]=val1&key[]=val2
のように、キーの後に [] をつけます。
パラメータ情報は
{"key"=>["val1", "val2"], "amp"=>nil, "controller"=>"params", "action"=>"show_param"}

となるので、params[:key][0], params[:key][1] としてアクセスできる。

3. ハッシュマップ
ひとつのパラメータキーに、ハッシュマップを対応させる場合の書式。Rails特有?
http://foo.com/params/show_param?user[name]=cocomo&user[gender]=F
のように、キーのあとにハッシュキーを[]で囲んで指定します。
パラメータ情報は
{"user"=>{"name"=>"cocomo", "gender"=>"F"}, "amp"=>nil, "controller"=>"params", "action"=>"show_param"}

となる。

ところで、ActiveRecordは new の引数にキーワード引数(ハッシュ)を渡すことができます。

それとこのパラメータの引渡し方と組み合わせることで、コントローラ内で、ActiveRecordのnewがとてもシンプルに

User.new(params[:user])

と書けることになります。(便利に使っていたけれど、こうなっていたのか。。。)

===

クエリストリングの解析はどうなっているのか、とRailsのコードを追ってみるとRack::Request クラスの GET メソッドに辿りつきます。

# rack/lib/rack/request.rb
module Rack
class Request
...
# Returns the data recieved in the query string.
def GET
if @env["rack.request.query_string"] == query_string
@env["rack.request.query_hash"]
else
@env["rack.request.query_string"] = query_string
@env["rack.request.query_hash"] =
Utils.parse_nested_query(query_string)
end
end
...


rack.request.query_hash が params の正体のようです。実際の解析処理はUtils.parse_nested_queryメソッドで行っているようなので、そっちを見てみます。


# rack/lib/rack/utils.rb
def parse_nested_query(qs, d = '&;')
params = {}

(qs || '').split(/[#{d}] */n).each do |p|
k, v = unescape(p).split('=', 2)
normalize_params(params, k, v)
end

return params
end
module_function :parse_nested_query

def normalize_params(params, name, v = nil)
name =~ %r([\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''

return if k.empty?

if after == ""
params[k] = v
elsif after == "[]"
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v)
else
params[k] << normalize_params({}, child_key, v)
end
else
params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v)
end

return params
end
module_function :normalize_params


ごちゃごちゃとしていますが、キーのあとが ""(空文字列)か、"[]"か、"[child_key]" かで場合分けをしていることが読み取れます。そして、どうやら(メソッド名が示すとおり)ネストしたパラメータキーもOKなようです。

ちょっと、実験。

その1。
クエリストリング
key[][]=val1&key[][]=val2
を与えた場合。
params は
{"key"=>[nil, nil], "controller"=>"params", "action"=>"show_param"}

となります。配列のネストは不可の様子。

その2。
user[name[first]]=John&user[name[family]]=Lennon
を与えた場合。
params は
{"user"=>{"name"=>{"first"=>"John", "family"=>"Lennon"}}, "controller"=>"params", "action"=>"show_param"}

となります。ちゃんと、ネストしたハッシュマップになっています。

その3。
child[][name]=Mai&child[][name]=Aki
を与えた場合。
params は
{"child"=>[{"name"=>"Mai"}, {"name"=>"Aki"}], "controller"=>"params", "action"=>"show_param"}

となります。配列の中にハッシュマップをネストさせてもOK。

その4。
person[child[]]=Mai&person[child[]]=Aki
とした場合。
params は
{"person"=>{"child"=>[nil, nil]}, "controller"=>"params", "action"=>"show_param"}

となります。ハッシュマップの中に配列をネストさせるのはできない様子(配列の値がnilになってしまっている)。

その5。
person[child[][name]]=Mai&person[child[][name]]=Aki
とした場合。
params は
{"person"=>{"child"=>[{"name"=>"Aki"}]}, "controller"=>"params", "action"=>"show_param"}

となります。child がハッシュマップの配列を指しているものの、最初のnameが2番目のnameで上書きされてしまっているため、これも意図したとおりにはなっていません。

# 要するに、配列はトップレベルでしか使えないということなんでしょうか。

[Rails][習作] 簡易ブックマークアプリ (4)

indexページにタグクラウドを追加してみます。
変更するのは、indexアクションとビューのみ。


# bookmarks_controller.rb
def index
bookmarks_all = Bookmark.find(:all, :order=>"created_at desc")
tags_all = Tag.find(:all)
# Tag Cloud (タグのリスト)作成
@tag_cloud = Array.new
tags_all.each do |t|
@tag_cloud << t.tag if !@tag_cloud.include? t.tag
end

# search by created date
year = params[:year]
month = params[:month]
if year != nil && month != nil
first_of_month = Time.mktime(year.to_i, month.to_i, 1, 0, 0)
first_of_next_month = first_of_month.next_month.beginning_of_month
@bookmarks = Bookmark.find(:all, :conditions=>"created_at >= '#{first_of_month}' AND created_at < '#{first_of_next_month}'", :order=>"created_at desc")
else
@bookmarks = bookmarks_all
end

# search by tags
# パラメータで渡されたタグで絞り込む
tag = params[:tag]
if tag != nil
@bookmarks = Bookmark.find(:all, :conditions=>"EXISTS (SELECT * FROM tags WHERE tags.bookmark_id = bookmarks.id AND tags.tag = '#{tag}')")
end

# 作成月のハッシュ
@created = Hash.new
bookmarks_all.each{ |bookmark|
year = bookmark.created_at.year
month = bookmark.created_at.month
if !@created.key? year
@created.store year, [month]
elsif !@created[year].include? month
@created[year] << month
end
}

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @bookmarks }
end
end




# index.html.erb (追加部分のみ)
# タグの表示と絞込みのリンク作成
<div style="font-weight: bold; text-align: center; color: green;">Tag Cloud</div>
<div style="margin: 3px; padding: 10px; border: solid 2px green;">
<% @tag_cloud.each do |tag| %>
<%= link_to tag, bookmarks_path(:tag=>tag) %>
<% end %>
</div>

右ペイン下の"Tag Cloud"ボックスが追加箇所になります。
画面は、Tag Cloud の "RubyOnRails"をクリックして絞り込んだ様子。
URL : http://hostname:port/bookmarks?tag=RubyOnRails

ところで、Rails って同名の複数リクエストパラメータを渡す場合はどうするのでしょうか。
普通に
http://hostname:port/bookmarks?tag=Rails&tag=RubyOnRails
とすると、
Parameters: {"tag"=>"RubyOnRails"}
と後のパラメータ値で上書きされてしまう。。。


<<[Rails][習作] 簡易ブックマークアプリ (3)