不格好エンジニア (引っ越しました)

wordpress.comから引っ越しました。

Rails::Railtieを流し読み

Rails::Railtieを流し読み

動機

request_storeを読んでいる途中で、結局途中でrailtiesにブチあたり、この辺ちゃんと読んだことないのが気持ち悪いな、という気持ちでコードリーディングを始めてみました。

version

2.3.0

ドキュメントを読んでみる

Ruby on Rails API

個人的に重要だと思った点をピックアップしてみます。

core of the Rails framework and provides several hooks to extend Rails and/or modify the initialization process.

rails frameworkのコアになっており、初期化プロセスを拡張するための仕組みを提供している。

Every major component of Rails (Action Mailer, Action Controller, Active Record, etc.) implements a railtie. Each of them is responsible for their own initialization.

主要なcomponentはrailtieを実装している。なるほど、Gemを読んでいく上ではこの辺の仕組みをわかってないとしんどそうです。

To add an initialization step to the Rails boot process from your railtie, just define the initialization code with the initializer macro: If specified, the block can also receive the application object, in case you need to access some application-specific configuration, like middleware:

自作のrailtieをRails の初期化プロセスに組み込むには、initialization codeを書くだけです、と。blockはapplication objectを受け取ることができて、application固有の設定にアクセスできます。

request_storeのソースコードをみてみると、こうなっております。

module RequestStore
  class Railtie < ::Rails::Railtie
    initializer "request_store.insert_middleware" do |app|
      if ActionDispatch.const_defined? :RequestId
        app.config.middleware.insert_after ActionDispatch::RequestId, RequestStore::Middleware
      else
        app.config.middleware.insert_after Rack::MethodOverride, RequestStore::Middleware
      end

      if ActiveSupport.const_defined?(:Reloader) && ActiveSupport::Reloader.respond_to?(:to_complete)
        ActiveSupport::Reloader.to_complete do
          RequestStore.clear!
        end
      elsif ActionDispatch.const_defined?(:Reloader) && ActionDispatch::Reloader.respond_to?(:to_cleanup)
        ActionDispatch::Reloader.to_cleanup do
          RequestStore.clear!
        end
      end
    end
  end
end

bulletはこんな感じです。

  if defined? Rails::Railtie
    class BulletRailtie < Rails::Railtie
      initializer 'bullet.configure_rails_initialization' do |app|
        app.middleware.use Bullet::Rack
      end
    end
  end

ざっくり眺める

Rails::Railtieはabstract class

直接インスタンス化はできないことがわかります。

rails/railtie.rb at master · rails/rails · GitHub

module Rails
  class Railtie
    # ...
    def initialize #:nodoc:
      if self.class.abstract_railtie?
        raise "#{self.class.name} is abstract, you cannot instantiate it directly."
      end
    end
  end
end  

主要なメソッドは、rake_tasks, console, runner, generatorsかな。。 ここで格納されたblockが、後に #each_registered_block(type, &block) の中で使用されるようです。

module Rails
  class Railtie
    # ...

      def rake_tasks(&blk)
        register_block_for(:rake_tasks, &blk)
      end

      def console(&blk)
        register_block_for(:load_console, &blk)
      end

      def runner(&blk)
        register_block_for(:runner, &blk)
      end

      def generators(&blk)
        register_block_for(:generators, &blk)
      end
      
      private
        # receives an instance variable identifier, set the variable value if is
        # blank and append given block to value, which will be used later in
        # `#each_registered_block(type, &block)`
        def register_block_for(type, &blk)
          var_name = "@#{type}"
          blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, [])
          blocks << blk if blk
          blocks
        end
        

処理の流れを追ってみる

エントリポイント

lib/rails/initializable.rb

initializerメソッドはRails::Initializableモジュールに定義されており、ここでInitializerインスタンスが initializersに格納されています。

module Rails
  module Initializable
    module ClassMethods
...
      def initializers
        @initializers ||= Collection.new
      end
    
      ..
      def initializer(name, opts = {}, &blk)
        raise ArgumentError, "A block must be passed when defining an initializer" unless blk
        opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
        initializers << Initializer.new(name, nil, opts, &blk)
      end
    end
  end
end  

lib/rails/initializable.rb

Rails::Applications#initialize!が呼ばれると、 initializersはRails::Initializable#run_initializersの中でrunされていきます。 tsortが何なのかは、Ruby's TSort explained を読めばわかりそうなのですが、いったんここでは先に進みます。

config/environment.rb

# Load the Rails application.
require_relative 'application'

# Initialize the Rails application.
Rails.application.initialize!

lib/rails/application.rb

module Rails
  class Application < Engine
...
    # Initialize the application passing the given group. By default, the
    # group is :default
    def initialize!(group = :default) #:nodoc:
      raise "Application has been already initialized." if @initialized
      run_initializers(group, self)
      @initialized = true
      self
    end
  end
end  

lib/rails/initializable.rb

module Rails
  module Initializable
...
    def run_initializers(group = :default, *args)
      return if instance_variable_defined?(:@ran)
      initializers.tsort_each do |initializer|
        initializer.run(*args) if initializer.belongs_to?(group)
      end
      @ran = true
    end
...

initializersはRails::Application#initializersとRails::Initializable#initializers があるが、モジュールのメソッドよりクラスで定義されているメソッドが優先され、Rails::Application#initializers が実行されます(実際に動かして確認しました)。

lib/rails/application.rb

module Rails
  class Application < Engine
...
   def initializers #:nodoc:
      Bootstrap.initializers_for(self) +
      railties_initializers(super) +
      Finisher.initializers_for(self)
    end
  end  
end  

Bootstrap initializersは下記の通り、applicationの準備をするものが返されます。 [:load_environment_hook, :load_active_support, :set_eager_load, :initialize_logger, :initialize_cache, :initialize_dependency_mechanism, :bootstrap_hook, :set_secrets_root]

railties initializersは Rails::Application 自身に定義されたものが返されます。

(byebug) initializers.map(&:name) [:set_load_path, :set_autoload_paths, :add_routing_paths, :add_locales, :add_view_paths, :load_environment_config, :prepend_helpers_path, :load_config_initializers, :engines_blank_point, :append_assets_path]

#railties_initializersの中身を少し掘ってみていきます。#ordered_railtiesは何をしているのでしょうか?

    def railties_initializers(current) #:nodoc:
      initializers = []
      ordered_railties.reverse.flatten.each do |r|
        if r == self
          initializers += current
        else
          initializers += r.initializers
        end
      end
      initializers
    end
    
    # Returns the ordered railties for this application considering railties_order.
    def ordered_railties #:nodoc:
      @ordered_railties ||= begin
        order = config.railties_order.map do |railtie|
          if railtie == :main_app
            self
          elsif railtie.respond_to?(:instance)
            railtie.instance
          else
            railtie
          end
        end

        all = (railties - order)
        all.push(self)   unless (all + order).include?(self)
        order.push(:all) unless order.include?(:all)

        index = order.index(:all)
        order[index] = all
        order
      end
    end
    

lib/rails/initializable.rb

ここで、定義されたブロックが実行されてますね。

class Initializer
  def run(*args)
    @context.instance_exec(*args, &block)
  end
end

次に読みたい

wicked_pdf, request_store, bullet, wkhtmltopdf, sidekiq, active_support, rbenv, webrick, draper, jbuilder, rails/active_job, rails/spring, capistrano, omniauth, committee etc..

参考

Railtieのinitializerが読み込まれる仕組み

Introduction to Railties

The Rails Initialization Process — Ruby on Rails Guides

Configure your gem the Rails way with Railtie

ブログに技術書の内容を丸写しする問題点と、オリジナルなコンテンツを書くためのアイデア - give IT a try

RubyGemコードリーディングのすすめ

#299 Rails Initialization Walkthrough (pro) - RailsCasts

vendor/bundle/ruby/2.3.0/gems/rails-4.2.5/guides/source/initialization.md · master · ypleung / cmpt276ass2 · GitLab

Ruby's TSort explained

[Ruby入門] 11. クラスを拡張する① モジュールのお話