respond_toを調べてみた

どうにも、Railsのrespond_toメソッドがなんでこんな記述で動くのか良く判らなかったので、じっくり調べてみた。

  • 基本の使い方のおさらい
    • まずは、コントローラにこんな感じで書いて、
       def index
         @books = Book.all
         respond_to do |format|
           format.html # index.html.erb
           format.json { render json: @books }
         end
       end
      
    • で、http://localhost:3000/books にアクセスすればHTMLが返却され、
    • http://localhost:3000/books.json にアクセスすればJSONが返却される、と。
  • たぶん、yieldを使って書いてありそうなんだけど、ちょっと理解しがたいのでRailsのソースを見てみた。
    • まずは、当たりも付かないので、Rubyの下をソースを全grepして、怪しいファイルを発見。うーん、かっこ悪いw
      lib\ruby\gems\1.9.1\gems\actionpack-3.1.0\lib\action_controller\metal\mime_responds.rb(42):       def respond_to(*mimes)
      lib\ruby\gems\1.9.1\gems\actionpack-3.1.0\lib\action_controller\metal\mime_responds.rb(191):     def respond_to(*mimes, &block)
      
    • まあ、この「mime_responds.rb」が怪しいっぽいので、ソースを見ると…
         include ActionController::ImplicitRender
         included do
           class_attribute :responder, :mimes_for_respond_to
           self.responder = ActionController::Responder
           clear_respond_to
         end
         module ClassMethods
           def respond_to(*mimes)
             options = mimes.extract_options!
             only_actions   = Array(options.delete(:only))
             except_actions = Array(options.delete(:except))
             new = mimes_for_respond_to.dup
             mimes.each do |mime|
               mime = mime.to_sym
               new[mime]          = {}
               new[mime][:only]   = only_actions   unless only_actions.empty?
               new[mime][:except] = except_actions unless except_actions.empty?
             end
             self.mimes_for_respond_to = new.freeze
           end
           def clear_respond_to
             self.mimes_for_respond_to = ActiveSupport::OrderedHash.new.freeze
           end
         end
         def respond_to(*mimes, &block)
           raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
           if response = retrieve_response_from_mimes(mimes, &block)
             response.call(nil)
           end
         end
         def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc:
           mimes ||= collect_mimes_from_class_level
           collector = Collector.new(mimes) { |options| default_render(options || {}) }
           block.call(collector) if block_given?
           if format = request.negotiate_mime(collector.order)
             self.content_type ||= format.to_s
             lookup_context.freeze_formats([format.to_sym])
             collector.response_for(format)
           else
             head :not_acceptable
             nil
           end
         end
         def collect_mimes_from_class_level #:nodoc:
           action = action_name.to_sym
           self.class.mimes_for_respond_to.keys.select do |mime|
             config = self.class.mimes_for_respond_to[mime]
             if config[:except]
               !action.in?(config[:except])
             elsif config[:only]
               action.in?(config[:only])
             else
               true
             end
           end
         end
      
  • む、これっぽいか?
  • おお?これって、ClassMethods::respond_to(*mimes)って、使ってる?うーん、良く判らん…。
  • ソース解析すると、こんな感じか?
    • まず、respond_to(*mimes, &block)が呼び出され、その中でmimesがあってかつblockが渡されてれば、retrieve_response_from_mimesを実行
    • retrieve_response_from_mimesだと、渡されたmimesが正しいかをcollect_mimes_from_class_levelでチェックして、collector を生成
      • collector は、mimesの分だけdefault_renderを使ってoptionsを作る
    • その後、block_given?でブロックがあれば、block.call(collector)で「ブロックで指定されたメソッドを実行」
      • なんだけど、どうやって「ブロックで指定されたメソッドを実行」してるんだ?
  • あ、判った。Rubyだとメソッド呼び出しでのdo ~ endが結構特殊なんだ!
    • 例えば、こんなメソッドを定義して、
      def foo(p1,p2,&block)
        block.call(p1,p2)
      end
      
    • こんなコードで呼び出すと、
      foo("p1","p2") do |p1,p2|
        p p1
        p p2
      end 
      
    • こんな感じで表示される
      "p1"
      "p2"
      
    • これは、block.call(p1,p2)実行時に、fooのパラメータのp1,p2で、do~end内が繰り返されずに実行されるんだねー。
      • だから、fooをこんな感じで書くと
        foo("p1","p2") do |p1,p2|
          p p2
          p p1
          p "dummy"
        end 
        
      • こんな感じで表示されるんだ!
        "p2"
        "p1"
        "dummy"
        
  • しかし、結局mimesとかは、何が入ってるんだか、良く判らんからちゃんと見てみると?
    • まずは、respond_to(*mimes, &block)で呼び出される時は、*mimesとなっている
      • *配列は、「配列でもらって、使う時は展開する」書式なんだー
      • なので、ここでは、retrieve_response_from_mimes(mimes=nil, &block)の時に、mimesの配列が列挙されるんだねー。
    • で、retrieve_response_from_mimes(mimes=nil, &block)では、列挙されたmimesが渡されてるはず、と。
      • あー、だから、retrieve_response_from_mimes内では、 以下のコードで「mime」のひとつずつに対して、true/false判定するんだねー
        mimes ||= collect_mimes_from_class_level
        

-
最終更新:2012年01月09日 08:11