Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2012-02-21

unite-outline の近況: 次期バージョンの開発

昨年の秋頃から目立った変化のない unite-outline ですが、開発が停滞しているとか、作者のやる気がなくなった*1とかではなく、「今年の抱負 - Vimプラギン編」で述べた通り、次期バージョンの開発(準備)を少しずつ進めています。

ある程度形になったら別ブランチを切って push しようと思ってますが、まだ仕様をあれこれ考えたり非同期実行のやり方を模索している段階で、ものは全然できておりません(汗 まあ、上半期のうちに最初のバージョンを push できたらいいな、ぐらいの見込みです。*2

大きな変更が必要になるついでに、「もうこの際、たまった負債を全部返済してやる!」みたいな意気込みでして、プラグインの構成上前から気になっていた点や、作者がまずいと思っている outline info の仕様など、全面的に見直してドカンと刷新する予定です。(というか、そうせざるを得ないのです…… 詳細は以下を参照)

で、その次期バージョンですが、青写真は以下の通りです。

NuOutline(仮)

アウトライン解析部を独立したプラグイン(NuOutline(仮))として切り出し、unite-outline をそのフロントエンドとして添付するという構成をとります。

プラグインの構成上一番大きな変更点がこれです。

「unite.vim の source として unite-outline は大き過ぎる」、「アウトライン解析を行うプラグインに unite-outline を添付すべきでは?*3」という指摘は以前から何度かあり、自分でも気になっていました。また、

  • autoload/unite/sources/outline/ 以下のディレクトリ階層の深い位置にファイルがごちゃごちゃと多過ぎる。
  • その結果、オートロード関数の名前がやたら長くなる。
  • また、動作が複雑になってオプション変数も増えたが、unite.vim の source のそれはやはり名前が長くなる。
  • unite.vim の source とアウトライン解析部が渾然一体、なんだかなー

といったこともあり、アウトライン解析を行う部分を独立したプラグインとして切り出し、プラグイン全体の構成を仕切り直すことにしました。図で示すと以下のようになります。

f:id:h1mesuke:20120221221020p:image

きれいに 3層に分離しました。3者の役割分担は Webアプリケーションなどでおなじみの MVCパターンでとらえると理解しやすいです。アウトライン表示のインターフェースを提供してくれる unite.vim がビュー、見出し抽出のロジックを持っている NuOutline がモデル、両者の間に入ってあれこれと調停をし、見出しの自動更新のための面倒をみたりする unite-outline がコントローラというわけですね。

V, C は交換可能(にしたい)

NuOutline は(頑張って)unite.vim, unite-outline と密に結合させないで作ろうと思っています。unite-outline はあくまでフロントエンドのデフォルト実装として NuOutline に添付されているもの、という位置付けにして、第三者が別のフロントエンドを提供できる余地を残しておきます。(そんな人が現れるかは別にして ;-) )

が、リポジトリは unite-outline のままで!

どちらかというと unite-outline の方が NuOutline のオマケ的な存在なのですが、Vundle や neobundle.vim 全盛(?)の昨今、リポジトリURL が変わるというのは致命的だと思うので、開発は引き続き、現行の unite-outline のリポジトリで行います。まあ、使うだけなら NuOutline は unite-outline の影に隠れて見えませんし、ユーザーが使うのはあくまで unite.vim を通してですからね。

というわけでなんと、unite-outline という unite.vim の source に、プラグインがまるまる1個付いてくるということになります(笑)

オプション変数名が変化

「unite.vim上でどう表示するか」に関する設定を除き、オプション変数の所管が NuOutline の方へ移るので変数の名前が変わってしまいます。しばらくは古い変数の方も見るようにしたいところですが……

outline info → parser

unite.vim の source である unite-outline の source……的な存在であった outline info は所管が NuOutline の方へ移ります。それにともない名称も outline info から parser に変更し、配置場所も

  • autoload/nuoutline/parsers/

とします。*4unite.vim における sources, kinds, filters などと同様の位置付けになります。

DRY でない create_heading() よ消え去れ!

outline info の仕様には(今となっては)気に入らない部分がたくさんあり、その最たるものが見出し抽出のためのパターンが複雑になると create_heading() の中身が DRY でなくなるというものです。例えば、

'^\s*\(foo\|bar\|baz\|fizz\|buzz\)\>'

のようなパターンを設定したとして、マッチングが行われ、マッチが見つかって create_heading() が呼び出されたとします。その場合、マッチしたサブマッチがどれかによって処理を分けなければならないことがほとんどで、現行の outline info ではマッチした文字列に対して再度マッチングを試み、サブマッチのうちのどれに実際にマッチしたのかを判定するような DRY でないコードが氾濫しています。

これは outline info を書く側の問題ではなくて、そのようにしか書けなくしている outline info の仕様の方に問題があるのです。

「こんなことでは outline info を書いてみようと思ってくれた人も離れていくわ!」

と強く思った私は、outline info の所管が NuOutline へ移るこのタイミングで、その仕様を刷新することにしました。

まだ仕様を練っている段階ですが、

let s:parser.foo_pattern = "foo's pattern"
function! s:parser.create_foo_heading()
endfunction

let s:parser.bar_pattern = "bar's pattern"
function! s:parser.create_bar_heading()
endfunction

let s:parser.baz_pattern = "baz's pattern"
function! s:parser.create_baz_heading()
endfunction

こんな風に、見出しの種類ごとに個別にパターンを設定できるようにし、「どのパターンにマッチしたか」の判定までを本体側の責任として、適切なコールバックが呼び出されるようにします。parser側では見出し(辞書)の作成に専念できるようになるので、パターンマッチ方式の parser を作るための敷居はかなり下がるはずです。

構文解析が必要になるケースのサポート

create_heading() について力説した直後にアレですが、「正規表現によるパターンマッチ → create_heading() 」というやり方で見出しが拾えるファイルタイプは実は少なく、ほとんどのプログラミング言語では見出しを「ちゃんと」拾うのに構文解析が必要になります。

具体的に言うと、ctags などの外部の構文解析プログラムを呼び出し、その出力を解析して見出しの木を parser が自前で作る必要があります。

今後、対応ファイルタイプを増やしていくためにも、その辺の処理が書きやすくなるようなユーティリティコードの充実やドキュメントの整備などが必要と考えています。

まとめ

最初からちゃんと設計しとけや! ヽ(*`Д´)ノゴルァ

うう、当初は「正規表現でマッチさせて拾うだけやん」程度の軽いノリで作り始めたのでございます。最初にコミットされたバージョンの素朴さがすべてを物語っている……

しかし、まあ、ここまできたらプラグインとしてちゃんとしたものにしたいですね!

*1:むしろ逆で、変なブーストが入ったw

*2:今年の抱負ということになってるので、遅くとも年内にはw

*3:alignta に unite-alignta が同梱されているのがこの構成ですね。

*4:outline info は autoload/outline/ に配置してもよいことになっていましたが、これは autoload/unite/sources/outline/ が自作の outline info を配置する場所として位置が深すぎるだろうとの配慮からでした。通常、autoload/ に何かを配置させるにあたっては、プラグイン名できちんと名前空間を切るべきです。

2010-02-09

Railsノート - Authlogic を読む (1) - Authlogic::ActsAsAuthentic

ユーザー認証のためのプラグインとして Authlogic を使うことに決めた。公式のチュートリアルに従いモデルやコントローラを生成、ヘルパー類もコピペして、無事に認証機能の組み込みに成功した。ログイン第一号を自分で飾って感慨無量。

参考:

と、ここまではいいのだが、正直、Authlogic が裏で何をやっているのかよくわからない(笑) ユーザー認証やセッションまわりがブラックボックスというのもなんだか危ういので、Authlogic が何をやっているか、コードを追って理解しようと思う。百聞は一読に如かず。参照する Authlogic のバージョンは 2.1.3。コメントなどは適当に省いてある。

Authlogic はユーザー認証のために、公式チュートリアルと同じクラス構成で導入したとする。(モデルとして User, UserSession, コントローラとして UserSessionsController の存在を前提する)

model User

まずは認証の対象となるモデル User から見ていこう。acts_as_authentic が Userモデルをどのように拡張するのか。

Authlogic導入直後の Userクラス↓

# app/models/user.rb

class User < ActiveRecord::Base
  acts_as_authentic
end

Authlogic::ActsAsAuthentic::Base

一連の ActsAsAuthentic::Xxxモジュールが認証の対象モデル(User)を拡張するコードになっている。その中心である acts_as_authentic/base.rb を見てみる。ここに acts_as_authentic の定義もある。

# authlogic/acts_as_authentic/base.rb

module Authlogic
  module ActsAsAuthentic
    module Base
      def self.included(klass)
        klass.class_eval do
          extend Config
        end
      end

      module Config
        def acts_as_authentic(unsupported_options = nil, &block)
          # snip
        end

このパターン頻出。かいつまんで言うと、

ActsAsAuthentic::Xxxモジュールをクラス Hoge に include すると、ActsAsAuthentic::Xxx::Config のインスタンスメソッドが Hoge のクラスメソッドになりますよ

ということ。なので、acts_as_authentic は ActiveRecord::Base のクラスメソッドとして呼び出すことができるわけだ。

先に書いてしまったが、ActsAsAuthentic::XxxモジュールActiveRecord::Base に include される。acts_as_authentic/base.rb の末尾には以下のような include がずらっと並ぶ。

::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::RestfulAuthentication
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::ValidationsScope

なんというか、下手にクラスメソッドなど自分で定義しようものなら知らず知らず上書きしてしまいそうな勢いである。

肝心の acts_as_authentic の定義

        def acts_as_authentic(unsupported_options = nil, &block)
          # snip
          yield self if block_given?
          acts_as_authentic_modules.each { |mod| include mod }
        end

あれこれ設定の機会を与えるために yield した後、モデル(User)にモジュールを include している。その内容をひらいて書くと

User.include Authlogic::ActsAsAuthentic::Email::Methods
User.include Authlogic::ActsAsAuthentic::LoggedInStatus::Methods
User.include Authlogic::ActsAsAuthentic::Login::Methods
 :
User.include Authlogic::ActsAsAuthentic::ValidationsScope::Methods

となる。*1ここで Userクラスに ActsAsAuthentic::Xxx::Methodsモジュールが include されていくことによって、Userクラスに validation が追加されたり、メソッドが追加されたりする。例えば、ActsAsAuthentic::Email::Methods などは、

# authlogic/acts_as_authentic/email.rb

      # All methods relating to the email field
      module Methods
        def self.included(klass)
          klass.class_eval do
            if validate_email_field && email_field
              validates_length_of email_field, validates_length_of_email_field_options
              validates_format_of email_field, validates_format_of_email_field_options
              validates_uniqueness_of email_field, validates_uniqueness_of_email_field_options
            end
          end
        end
      end

こんな風に属性値の長さや書式に関する validation を Userクラスに追加する。

次回

acts_as_authentic が Userモデルをどのように拡張するのかわかったので、今回はここまで。

次は、User に include される ActsAsAuthentic::Xxx::Methodsモジュールのうち、認証機能に関わる重要な部分を取り上げて見ていこうと思う。

*1:補足:ActsAsAuthentic::Password は Methods以外に Callbacksモジュールも include する。

2010-02-07

Railsノート - セッションまわりを読む (2) - セッションの保存/復元のロジック

途中で力尽きたというか、こういうリーディングのやり方は効率に問題があると悟った(笑)ので、後半ぐだぐだですが、やった分だけうpします。

セッションCookie

セッションCookie に保存されるロジックから見た方がわかりやすいと思うのでそっちから。CookieStore.call(env) の後半部分(@app.call(env) の後)を見る。

# action_controller/session/cookie_store.rb

module ActionController
  module Session
    class CookieStore

      # snip

      def call(env)
        env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
        env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup

        status, headers, body = @app.call(env)

        session_data = env[ENV_SESSION_KEY]
        options = env[ENV_SESSION_OPTIONS_KEY]

        if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
          session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
          session_data = marshal(session_data.to_hash)

          raise CookieOverflow if session_data.size > MAX

          cookie = Hash.new
          cookie[:value] = session_data
          unless options[:expire_after].nil?
            cookie[:expires] = Time.now + options[:expire_after]
          end

          cookie = build_cookie(@key, cookie.merge(options))
          unless headers[HTTP_SET_COOKIE].blank?
            headers[HTTP_SET_COOKIE] << "\n#{cookie}"
          else
            headers[HTTP_SET_COOKIE] = cookie
          end
        end

        [status, headers, body]
      end

セッションCookie の「値」へと永続化しているのはこの部分↓

session_data = marshal(session_data.to_hash)

marshal の定義は

        # Marshal a session hash into safe cookie data. Include an integrity hash.
        def marshal(session)
          @verifier.generate(persistent_session_id!(session))
        end

persistent_session_id! は新規セッションセッションID を含める処理。クッキーストアの場合、Cookie そのものがセッションなので紐付けのためのセッションID は本来不要なはずだが、コントローラはそんなこと知らない。

@verifier ってなんなんじゃコラ。

      def initialize(app, options = {})
        # snip
        @secret = options.delete(:secret).freeze

        @digest = options.delete(:digest) || 'SHA1'
        @verifier = verifier_for(@secret, @digest)
        # snip
      end

      # snip

        def verifier_for(secret, digest)
          key = secret.respond_to?(:call) ? secret.call : secret
          ActiveSupport::MessageVerifier.new(key, digest)
        end

セッションCookie の「値」へと(改竄/偽造予防の措置をとりつつ)永続化するロジックを持っているのがこの ActiveSupport::MessageVerifier というクラス。

ActiveSupport::MessageVerifier
# activesupport/lib/active_support/message_verifier.rb 

module ActiveSupport
  class MessageVerifier

    def initialize(secret, digest = 'SHA1')
      @secret = secret
      @digest = digest
    end

    # snip

    def generate(value)
      data = ActiveSupport::Base64.encode64s(Marshal.dump(value))
      "#{data}--#{generate_digest(data)}"
    end

    # snip

      def generate_digest(data)
        require 'openssl' unless defined?(OpenSSL)
        OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data)
      end
  end
end

この辺は以下のエントリで解説されている通りだ。*1

Cookieセッション

めんどくさいので省略*2 → まとめ参照

まとめ

ファジーなまとめ。

AbstractStore::SessionHash
 ↓
marshal
 ↓
ActiveSupport::MessageVerifier#generate → generate_digest
 ↓
Cookie
 ↓
unmarshal
ActiveSupport::MessageVerifier#verify → secure_compare
 ↓
AbstractStore::SessionHash

*1:リンク先エントリの投稿日付を見るにつけ、おいおい俺って何周遅れなんだよ、と気が滅入るばかり(笑)

*2:こんな調子でやってたらほとんどすべてのコードを貼り付けなければならなくなる。時間がかかってしょうがないw リーディングのやり方をもうちょっと効率的なものにする必要がある。

2010-02-06

Railsノート - セッションまわりを読む (1) - セッションの保存/復元のタイミング

Railsセッションまわりについて調べる。知りたいのは (1) Cookie に含まれている情報からセッション(sessionメソッドの返値となるオブジェクト)を作るところと、その逆に、(2) セッションCookie としてレスポンスに含めるところだ。イメージとしては、

セッションCookie ⇔ リクエスト/レスポンス

というような関係にあると思うのだが、その辺を確認したい。セッションストアは Cookie Store とする。例のごとく、参照する Rails のコードは 2.3.5

Cookie

Cookie としてクライアントに送信されるセッションは以下のようになる。(sandbox はアプリケーションの名前)

_sandbox_session=BAh7BjoPc2Vzc2lvbl9pZCIlN2UwZTA5MTQ2NWVjY2Q4NjYxMjBlMjI4YWEzZWMxZDg%3D--8bfad135e5bb257c95eb5438625d6d058b5b9636; path=/; domain=localhost; HttpOnly

この Cookie の中身に関しては以下のエントリが非常に参考になる。感謝。

今回確認したいのはクライアントから送られてきた上記のような Cookie からセッションが復元されるところと、その逆に、セッションCookie に保存されるところだ。

では、ActionController::Base の sessionメソッドの定義から、順番に見ていこう。

ActionController::Base

# actionpack/lib/action_controller/base.rb

    # Holds a hash of objects in the session. Accessed like <tt>session[:person]</tt> to get the object tied to the "person"
    # key. The session will hold any type of object as values, but the key should be a string or symbol.
    attr_internal :session

attr_internal については前に調べた。セッションは @_session というインスタンス変数に入る。で、@_session はコントローラの initialize に相当する assign_shortcuts において以下のように定義される。

      def assign_shortcuts(request, response)
        @_request, @_params = request, request.parameters

        @_response         = response
        @_response.session = request.session

        @_session = @_response.session
        @template = @_response.template

        @_headers = @_response.headers
      end

@_session = @_response.session = request.session

つまり、request.session と @_response.session は同一のオブジェクトを指しており、コントローラの sessionメソッドはそのショートカットである。

ActionController::Request

module ActionController
  class Request < Rack::Request

    # snip

    def session
      @env['rack.session'] ||= {}
    end

    def session=(session) #:nodoc:
      @env['rack.session'] = session
    end

    def reset_session
      @env['rack.session.options'].delete(:id)
      @env['rack.session'] = {}
    end

    def session_options
      @env['rack.session.options'] ||= {}
    end

    def session_options=(options)
      @env['rack.session.options'] = options
    end

CookieJar と違い、セッションはただのハッシュのようだ。

ここにはクライアントから送られてきた Cookie からセッションストアの種類に応じたやり方でセッションを復元(deserialize)するコードはない。それを探す。actionpack/lib/action_controller.rb にて autoload されているモジュールの中にそれがあるはずだ。まず、コントローラは使用するセッションストアの種類を知る必要がある。

ActionController::SessionManagement

# actionpack/lib/action_controller/session_management.rb

module ActionController #:nodoc:
  module SessionManagement #:nodoc:
    def self.included(base)
      base.class_eval do
        extend ClassMethods
      end
    end
    
    module ClassMethods
      # Set the session store to be used for keeping the session data between requests.
      # By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
      # but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
      # <tt>:mem_cache_store</tt>, or your own custom class.
      def session_store=(store)
        if store == :active_record_store
          self.session_store = ActiveRecord::SessionStore
        else
          @@session_store = store.is_a?(Symbol) ?
            Session.const_get(store.to_s.camelize) :
            store     
        end
      end
        
      # Returns the session store class currently used.
      def session_store
        if defined? @@session_store
          @@session_store
        else
          Session::CookieStore
        end
      end

session_store が返すのはセッションの保存/復元ロジックを持ったクラス。これを別のものに置き換えることでコントローラ側はコード修正なしに複数のセッションストアに対応できる。いわゆる Strategyパターン。

では、この session_store が返すクラスを使ってセッションの保存/復元を行っている箇所はどこだろう。

middlewares.rb

# actionpack/lib/action_controller/middlewares.rb

use "Rack::Lock", :if => lambda {
  !ActionController::Base.allow_concurrency
}

use "ActionController::Failsafe"

use lambda { ActionController::Base.session_store },
    lambda { ActionController::Base.session_options }

use "ActionController::ParamsParser"
use "Rack::MethodOverride"
use "Rack::Head"

use "ActionController::StringCoercion"

なんと、セッションの復元/保存はミドルウェアの層で処理されていた。ということは、セッションストアクラス(ここでは CookieStore)は Rack application object ということになる。call(env) にセッションの保存と復元のコードがあるはず。

ActionController::Session::CookieStore

# action_controller/session/cookie_store.rb

module ActionController
  module Session
    class CookieStore

      # snip

      def call(env)
        env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
        env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup

        status, headers, body = @app.call(env)

        session_data = env[ENV_SESSION_KEY]
        options = env[ENV_SESSION_OPTIONS_KEY]

        if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
          session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
          session_data = marshal(session_data.to_hash)

          raise CookieOverflow if session_data.size > MAX

          cookie = Hash.new
          cookie[:value] = session_data
          unless options[:expire_after].nil?
            cookie[:expires] = Time.now + options[:expire_after]
          end

          cookie = build_cookie(@key, cookie.merge(options))
          unless headers[HTTP_SET_COOKIE].blank?
            headers[HTTP_SET_COOKIE] << "\n#{cookie}"
          else
            headers[HTTP_SET_COOKIE] = cookie
          end
        end

        [status, headers, body]
      end

ここだな。

  1. Cookie からセッションを復元(※実際には遅延ロード → 補足)
  2. アプリケーション実行
  3. セッションCookie に保存

と非常にシンプルだ。

セッションの保存/復元(※実際には遅延ロード → 補足)がミドルウェアの層で行われることがわかったので、次は保存と復元のロジックを個別に見ていこうと思う。

補足:SessionHash は Cookie からのセッションのロード(復元)をハッシュの要素が実際に参照されるまで遅延するので、実際の復元タイミングはもう少し後になる。コントローラセッションを一切使わなければ、当然復元もされない。上記のコードにおいて、セッションCookie に保存するにあたり、session_data.send(:loaded?) でセッションが実際にロードされたかどうかを確かめているのもそのため。

2010-02-05

Railsノート - ActionController::Request の生成過程を Webサーバーまでさかのぼる

Cookie について調べたときに ActionController::Requestクラスについてちょっと触れたので、今回はこれが new されるところを見てみようと思う。クライアントからの HTTPリクエストがサーバーの 80番ポートに届いて、それが Webサーバーに捌かれてアプリケーションに振り分けられ、ActionController::Request というオブジェクトになる、まさにその瞬間というか、そこへ至る過程を理解したい。

参照する Rails のバージョンは 2.3.5。コメントなどは適当に省いてある。

ActionController::Request

まずは ActionController::Request の initialize からスタート。
ここから順次その呼び出し元を辿っていこう。

# actionpack/lib/action_controller/request.rb

module ActionController
  class Request < Rack::Request

ここに initialize はなし。Rack::Request の方を見る。

Rack::Request
# rack/request.rb

module Rack
  class Request
    # The environment of the request.
    attr_reader :env

    def initialize(env)
      @env = env
    end

env は環境変数だろう。以後のソースを読む限りこれはハッシュだ。知りたいのはこのハッシュが作られるところ、そして、それを渡して Request.new を呼び出すところだ。

ActionController::Base

Request.new を探す。initialize にあるかと思ったが、そもそも ActionController::Base には initialize がない。initialize に @request = Request.new(env) なんてのがあるのを期待したのだが。

# actionpack/lib/action_controller/base.rb

module ActionController #:nodoc:
  class Base

  #snip

    class << self
      def call(env)
        # HACK: For global rescue to have access to the original request and response
        request = env["action_controller.rescue.request"] ||= Request.new(env)
        response = env["action_controller.rescue.response"] ||= Response.new
        process(request, response)
      end

      # Factory for the standard create, process loop where the controller is discarded after processing.
      def process(request, response) #:nodoc:
        new.process(request, response)
      end

Request.new とコントローラの new を同時に発見。これを見る限り、ActionController::Base というのは利用側のコードで new して使うものではないようだ。利用側のコードは ActionController::Base.call(env) を呼び出す。

ちなみに、インスタンスメソッドの方の process の定義はこちら↓

      # Extracts the action_name from the request parameters and performs that action.
      def process(request, response, method = :perform_action, *arguments) #:nodoc:
        response.request = request

        initialize_template_class(response)
        assign_shortcuts(request, response)
        initialize_current_url
        assign_names

        log_processing
        send(method, *arguments)

        send_response
      ensure
        process_cleanup
      end

すごいメソッドに遭遇。これは、「リクエストを受け取って、何らかの処理をして、レスポンスを返す」という Webアプリケーションのまさに肝ではないか。名前の通り、Webアプリケーションの process そのものだ。

待て。この process に渡されている request こそがコントローラの requestメソッドの返値になるはずなのだが、これをインスタンス変数に格納している箇所がない。アクセサの定義を探す。

    # Holds the request object that's primarily used to get environment variables through access like
    # <tt>request.env["REQUEST_URI"]</tt>.
    attr_internal :request

attr_internal というなんだか見慣れないものを発見。リファレンスを見るに、これは attr の拡張版でメソッド名と対応するインスタンス変数名との関係をイコールでなくすものだ。つまり @request を探してもダメだ。

じゃあどんな名前になるんだよってことでソースを追う。

# active_support/core_ext/module/attr_internal.rb

  # Declares an attribute reader and writer backed by an internally-named instance
  # variable.
  def attr_internal_accessor(*attrs)
    attr_internal_reader(*attrs)
    attr_internal_writer(*attrs)
  end

  alias_method :attr_internal, :attr_internal_accessor

  private
    mattr_accessor :attr_internal_naming_format
    self.attr_internal_naming_format = '@_%s'

    def attr_internal_ivar_name(attr)
      attr_internal_naming_format % attr
    end

どうやら、@_request という名前のインスタンス変数が使われているらしい。探す。

      def assign_shortcuts(request, response)
        @_request, @_params = request, request.parameters

        @_response         = response
        @_response.session = request.session

        @_session = @_response.session
        @template = @_response.template

        @_headers = @_response.headers
      end

見っけ。これは先ほどの process から呼び出されるから、これが実質コントローラの initialize のようなものだな。

Requestオブジェクトの生成が ActionController::Base.call(env) でなされるとわかったので、次はこの call(env) の呼び出し元を探す。

一時停止

ActionController::Base.call(env) を呼び出している箇所をうまく見つけられない。というか、call(env) という形のメソッド呼び出しが結構あって、どれが問題の call(env) やら。

待て待て。クラスとして定義されているコントローラは数あれど、「リクエスト → 処理 → レスポンス」という一連のセッションにおいて、インスタンスとして存在しているコントローラはひとつだけだ。つまり、リクエストから使用するコントローラ(とアクション)を決定している部分に、ActionController::Base.call(env) の呼び出しがあるはず。

というわけで、ルーティングの部分を見てみよう。

ActionController::Routing

# actionpack/lib/action_controller/routing.rb

module ActionController
  module Routing

    # snip

    Routes = RouteSet.new

使用するコントローラを解決するためにはルートセットを参照する必要がある。言い方を換えると、RouteSet はリクエストをコントローラに解決する能力がある。なので、その部分のコードを見てみよう。そこか、もしくはそこを呼び出している箇所に ActionController::Base.call(env) の呼び出しがあるはずだ。

ActionController::Routing::RouteSet

# actionpack/lib/action_controller/routing/route_set.rb

module ActionController
  module Routing
    class RouteSet #:nodoc:

      # snip

      def call(env)
        request = Request.new(env)
        app = Routing::Routes.recognize(request)
        app.call(env).to_a
      end

      def recognize(request)
        params = recognize_path(request.path, extract_request_environment(request))
        request.path_parameters = params.with_indifferent_access
        "#{params[:controller].to_s.camelize}Controller".constantize
      end

これだ! recognize が返すのはリクエストを処理できるコントローラのクラスで、RouteSet#call(env) は recognize が返したコントローラクラスに対し、call(env) を呼び出している。これがまさに ActionController::Base.call(env) の呼び出しだ。(クラスをまたぐ場合のインターフェイスって call(env) に限定されてる? なんかやたら目につくぞ call(env) という形の呼び出し)

ActionController::Base.call(env) の呼び出しも見つかったし、次は RouteSet#call(env) を呼び出している箇所を探そう。これを呼び出しているのはどこか。

ActionController::Dispatcher

Routes.call で探すと一発で見つかった。名前もそのまんまだ。

# actionpack/lib/action_controller/dispatcher.rb

module ActionController
  # Dispatches requests to the appropriate controller and takes care of
  # reloading the app after each request when Dependencies.load? is true.
  class Dispatcher

    # snip

    def dispatch
      begin
        run_callbacks :before_dispatch
        Routing::Routes.call(@env)
      rescue Exception => exception
        if controller ||= (::ApplicationController rescue Base)
          controller.call_with_exception(@env, exception).to_a
        else
          raise exception
        end
      ensure
        run_callbacks :after_dispatch, :enumerator => :reverse_each
      end
    end

この dispatch を呼んでいる箇所がすぐ下にあった。

    def call(env)
      if @@cache_classes
        @app.call(env)
      else
        Reloader.run do
          # When class reloading is turned on, we will want to rebuild the
          # middleware stack every time we process a request. If we don't
          # rebuild the middleware stack, then the stack may contain references
          # to old classes metal classes, which will b0rk class reloading.
          build_middleware_stack
          @app.call(env)
        end
      end
    end

    def _call(env)
      @env = env
      dispatch
    end

    # snip

    private
      def build_middleware_stack
        @app = @@middleware.build(lambda { |env| self.dup._call(env) })
      end
  end
end

やっぱりここにも call(env)

どうもこの call(env) というメソッドがクラス間の共通インターフェイスになっていて、その呼び出しがチェーンになっているようだ。

Reloader とか middleware とか、なんだかよくわからないものが色々と出てきた。参ったなー

ミドルウェア

ミドルウェアにについては長くなるので省略。というか、Rack application object の仕様とかが絡んできてかなり脱線*1する。調べた結果をうまくまとめられたら別エントリで。

ともかく、@app.call(env) が dispatch を呼び出すことは確認したので、エントリの趣旨に立ち返って、Dispatcher#call(env) の呼び出し元を探す。

Rack

ここから先はどうやら Rails の外、Rack の領分ということになるようだ。Rails同梱のコードで Dispatcher.new があるのは開発時に WEBrick や Mongrel を起動するのに使われる railties/lib/commands/server.rb のみ。

# railties/lib/commands/server.rb

# snip
else
  require RAILS_ROOT + "/config/environment"
  inner_app = ActionController::Dispatcher.new
end

# snip

app = Rack::Builder.new {
  use Rails::Rack::LogTailer unless options[:detach]
  use Rails::Rack::Debugger if options[:debugger]
  map map_path do
    use Rails::Rack::Static
    run inner_app
  end
}.to_app

# snip

begin
  server.run(app, options.merge(:AccessLog => []))
ensure
  puts 'Exiting'
end

ActionController::Dispatcher.new の返値が Rack application object としての Railsアプリケーション。Rack は Webサーバーと Railsアプリケーションの間に入り、リクエストから環境変数のハッシュを作り、アプリケーションを起動(call(env))する。

例えば、Mongrel の場合、Rack::Handler::Mongrel が抽象化のための緩衝材として Mongrel と Rails の間に入る。

# rack/handler/mongrel.rb

module Rack
  module Handler
    class Mongrel < ::Mongrel::HttpHandler

      # snip

      def process(request, response)
        env = {}.replace(request.params)
        env.delete "HTTP_CONTENT_TYPE"
        env.delete "HTTP_CONTENT_LENGTH"
          
        env["SCRIPT_NAME"] = ""  if env["SCRIPT_NAME"] == "/"
          
        rack_input = request.body || StringIO.new('')
        rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
        
        env.update({"rack.version" => [1,0],
                     "rack.input" => rack_input,
                     "rack.errors" => $stderr,

                     "rack.multithread" => true,
                     "rack.multiprocess" => false, # ???
                     "rack.run_once" => false,

                     "rack.url_scheme" => "http",
                   })
        env["QUERY_STRING"] ||= ""
        env.delete "PATH_INFO"  if env["PATH_INFO"] == ""

        status, headers, body = @app.call(env)

        begin
          response.status = status.to_i
          response.send_status(nil)

          headers.each { |k, vs|
            vs.split("\n").each { |v|
              response.header[k] = v
            }
          }
          response.send_header

          body.each { |part|
            response.write part
            response.socket.flush
          }
        ensure
          body.close  if body.respond_to? :close
        end
      end
    end
  end
end

process を呼び出すのは Mongrel。環境変数をハッシュにまとめ、アプリケーションに対して call(env) を呼び出している。@app は Rack application object としての Railsアプリケーション(とミドルウェアのスタック)。よって、この @app.call(env) こそが、ここまでさかのぼってきた call(env) の呼び出しチェーンの起点である。

コントローラからさかのぼること幾多、とうとう Webサーバーに到達!

まとめ

ファジーなまとめ。

Webサーバー
 ↓
Rack::Handler::Hoge <-(app)- Rack::Builder
 ↓
ActionController::Dispatcher#call(env)
 ↓
@app.call(env)
 ↓
ActionController::Dispatcher#_call(env)
 ↓
ActionController::Dispatcher#dispatch
 ↓
ActionController::Routing::Routes.call(env)
ActionController::Routing::RouteSet#call(env)
 ↓
ActionController::Base.call(env) → Request.new(env)
 ↓
ActionController::Base#process

*1:しかし、その仕様を読んで目から鱗というか。だから入り口は call(env) なのかという発見があったり。

2010-02-04

Railsノート - Cookieまわりを読む (1) -Cookies@ActionController on Rack

前回Rails とはなんの関係もない Cookie そのもののお勉強だったが、今回からは Rails のコードを追いかけながらやる。Rails 3 旋風が吹き荒れる中、粛々と 2.3.5 のコードを読む。*1

AWDR2 をひも解く。(括弧で囲んだ数字は自分で入れた)

Rails では、便利でシンプルなインタフェースで cookie を抽象化しています。コントローラ属性の cookies は、cookie のプロトコルをラップするハッシュに似たオブジェクトです。(1)リクエストが来ると、ブラウザからアプリケーションに送信された cookie の名前と値で、cookies オブジェクトが初期化されます。アプリケーションは、いつでも新しいキー/値のペアを cookies オブジェクトに追加できます。(2)これらは、リクエストの初期化が終わるとブラウザに送信されます。

(1), (2) に該当する部分を見ていこう。参照する Rails のバージョンは 2.3.5。コメントなどは適当に省いてある。

まずは、コントローラの cookies から。

ActionController

# actionpack/lib/action_controller.rb

module ActionController
  # TODO: Review explicit to see if they will automatically be handled by
  # the initilizer if they are really needed.
  def self.load_all!
    [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
  end   

  autoload :Base, 'action_controller/base'
  autoload :Benchmarking, 'action_controller/benchmarking'
  autoload :Caching, 'action_controller/caching'
  autoload :Cookies, 'action_controller/cookies'

autoload の指定を発見。action_controller/cookies.rb には Cookies というモジュールがあり、cookies はそこに定義されたメソッドだった。ということは include されているはずだが、ここにあるのは autoload の指定のみ。Cookiesモジュールを include している箇所を探す。

調べたところ、これを include しているのは ActionController::Base であった。

ActionController::Base

# actionpack/lib/action_controller/base.rb

module ActionController #:nodoc:
  class Base

  # snip

  Base.class_eval do
    [ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers,
      Cookies, Caching, Verification, Streaming, SessionManagement,
      HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods,
      RecordIdentifier, RequestForgeryProtection, Translation
    ].each do |mod|
      include mod
    end
  end
end

Base が autoload され、require されて評価されると、サブモジュールが順次参照(autoload)されて Base に include される。

ActionController自体は名前空間を提供するためのモジュールなので、ここに include しても意味がない。コントローラ「クラス」のベースはあくまで ActionController::Base なので、当然 Cookiesモジュールもこのクラスに include される。考えたら当たり前のことだった。

Cookies

# actionpack/lib/action_controller/cookies.rb

  module Cookies
    def self.included(base)
      base.helper_method :cookies
    end

    protected
      # Returns the cookie container, which operates as described above.
      def cookies
        @cookies ||= CookieJar.new(self)
      end
  end

これが Base に include される Cookiesモジュール

cookiesメソッドの返値の実体は CookieJar オブジェクトで、「ハッシュによく似たオブジェクト」とはこれのことだろう。初期化に使われる self はコントローラ。ここからリクエストヘッダを参照するはず。

CookieJar
  class CookieJar < Hash #:nodoc:
    def initialize(controller)
      @controller, @cookies = controller, controller.request.cookies
      super()
      update(@cookies)
    end

ハッシュによく似たというか、ハッシュそのものだった。

ハッシュを初期化するにあたっては controller.request.cookies を使っている。というか、Hash#update はハッシュを受け取るので、controller.request.cookies の時点で既にハッシュにはなっているようだ。とすると、あえて CookieJar というクラスに変換するからには、このクラスに Cookie を扱う上でおいしいメソッドが定義されているのだろう。

    # Sets the cookie named +name+. The second argument may be the very cookie
    # value, or a hash of options as documented above.
    def []=(key, options)
      if options.is_a?(Hash)
        options.symbolize_keys!
      else 
        options = { :value => options }
      end
  
      options[:path] = "/" unless options.has_key?(:path)
      super(key.to_s, options[:value])
      @controller.response.set_cookie(key, options)
    end

    # Removes the cookie on the client machine by setting the value to an empty string
    # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
    # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
    def delete(key, options = {})
      options.symbolize_keys!
      options[:path] = "/" unless options.has_key?(:path)
      value = super(key.to_s)
      @controller.response.delete_cookie(key, options)
      value
    end

それがこの辺かな。ハッシュの更新が @controller.response.set_cookie, @controller.response.delete_cookie によって responseオブジェクトに反映されるようになっている。

このクラス、見事にリクエストとレスポンスの仲立ちをしている↓

controller.request.cookies -(initialize)-> CookieJar -([]=, delete)-> 
controller.response.set/delete_cookie

次は、CookieJar が仲立ちをしているところの、リクエスト/レスポンスの両クラスを見てみる。コントローラにおける request.cookies と response.set/delete_cookie の定義部分だ。

ActionController::Request

module ActionController
  class Request < Rack::Request

ActionController::Request は Rack::Request のサブクラス。ここに cookiesメソッドの定義は見当たらない、Rack::Request の方か。

Rack::Request
# rack.rb

module Rack
  # snip
  autoload :Request, "rack/request"
  autoload :Response, "rack/response"

おなじみの autoload。

Rack::Request が new されるところも見たいのだがそれは今度。今回はリクエストの Cookieヘッダがハッシュになるところを見るのが目的。

# rack/request.rb

module Rack
  class Request

  # snip

    def cookies
      return {}  unless @env["HTTP_COOKIE"]

      if @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"]
        @env["rack.request.cookie_hash"]
      else
        @env["rack.request.cookie_string"] = @env["HTTP_COOKIE"]
        # According to RFC 2109:
        #   If multiple cookies satisfy the criteria above, they are ordered in
        #   the Cookie header such that those with more specific Path attributes
        #   precede those with less specific.  Ordering with respect to other
        #   attributes (e.g., Domain) is unspecified.
        @env["rack.request.cookie_hash"] =
          Utils.parse_query(@env["rack.request.cookie_string"], ';,').inject({}) {|h,(k,v)|
            h[k] = Array === v ? v.first : v
            h
          }
      end
    end

これだな。@env["rack.request.cookie_string"] にはリクエストの Cookieヘッダの内容が入っており、@env["rack.request.cookie_hash"] にはそこから組み立てられたハッシュが入る。CookieJar を new するときに使われるのがこのハッシュだ。

リクエスト → CookieJar の流れはわかったので、次はレスポンスの方を見る。

ActionController::Response

# action_controller/response.rb

module ActionController # :nodoc:
  class Response < Rack::Response

  # snip

    def set_cookie(key, value)
      if value.has_key?(:http_only)
        ActiveSupport::Deprecation.warn(
          "The :http_only option in ActionController::Response#set_cookie " +
          "has been renamed. Please use :httponly instead.", caller)
        value[:httponly] ||= value.delete(:http_only)
      end

      super(key, value)
    end

こちらもやはり Rack::Response のサブクラス。ここには警告表示のためのコードしかない。set_cookie, delete_cookie ともに定義は Rack::Response の方にあるようだ。

Rack::Response
# rack/response.rb
module Rack
  class Response

  # snip

    def set_cookie(key, value)
      case value
      when Hash
        domain  = "; domain="  + value[:domain]    if value[:domain]
        path    = "; path="    + value[:path]      if value[:path]
        # According to RFC 2109, we need dashes here.
        # N.B.: cgi.rb uses spaces...
        expires = "; expires=" + value[:expires].clone.gmtime.
          strftime("%a, %d-%b-%Y %H:%M:%S GMT")    if value[:expires]
        secure = "; secure"  if value[:secure]
        httponly = "; HttpOnly" if value[:httponly]
        value = value[:value]
      end
      value = [value]  unless Array === value
      cookie = Utils.escape(key) + "=" +
        value.map { |v| Utils.escape v }.join("&") +
        "#{domain}#{path}#{expires}#{secure}#{httponly}"

      case self["Set-Cookie"]
      when Array
        self["Set-Cookie"] << cookie
      when String
        self["Set-Cookie"] = [self["Set-Cookie"], cookie]
      when nil
        self["Set-Cookie"] = cookie
      end
    end

    def delete_cookie(key, value={})
      unless Array === self["Set-Cookie"]
        self["Set-Cookie"] = [self["Set-Cookie"]].compact
      end

      self["Set-Cookie"].reject! { |cookie|
        cookie =~ /\A#{Utils.escape(key)}=/
      }

      set_cookie(key,
                 {:value => '', :path => nil, :domain => nil,
                   :expires => Time.at(0) }.merge(value))
    end

最終的にちゃんとレスポンスの Set-Cookieヘッダに反映されるようになっている。

まとめ

ファジーなまとめ。矢印の意味などはファジーな解釈で。

クライアント
 ↓
リクエスト(Cookieヘッダ)
 ↓
ActionController::Request < Rack::Request
 ↓ #cookies
ActionController::CookirJar → ActionController::Base#cookies
 ↓
ActionController::Response < Rack::Response
 ↓
レスポンス(Set-Cookieヘッダ * N)
 ↓
クライアント
寄り道

実は Rack について名前ぐらいしか知らなかったため、何をするものか調べてみた。

Webサーバーと Webアプリフレームワークの間にはさむ抽象化レイヤー

*1:と言いつつ、モチベーション下降気味。しかしながら、これは Rails を知るためという以上に Webアプリケーションというものを理解するためにやっているので、きっと無駄にはならないはず。