respond_to について調べてみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。クラスメソッドの稲毛です。

respond_to は、Ruby on Rails のアクションメソッド内でリクエストフォーマット(html, json, etc...)に応じた処理の記述で見かけます。

respond_to do |format|
  format.html { ... }
  format.json { ... }
end

どのような仕組みで動作しているのか気になっていたので少し調べてみました。

処理の流れ

おおまかな処理の流れは下記のようになっていました。

  1. respond_to メソッドの呼び出し
  2. Collector オブジェクトの生成
  3. respond_to 引数ブロックの評価
  4. Mime タイプ毎の処理を収集
  5. Collector から response の取得
  6. response の実行

respond_to メソッドの呼び出し

省略されている丸括弧を付与し簡略化すると下記のようになります。

respond_to() do..end

これはブロック付きメソッド呼び出しなので、respond_to メソッドでは yield でブロックを評価するか Proc オブジェクトとして受け取っている筈ですね。

respond_to メソッドはモジュール ActionController::MimeResponds に定義されています。

actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb
    def respond_to(*mimes, &block)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      if collector = retrieve_collector_from_mimes(mimes, &block)
        response = collector.response
        response ? response.call : render({})
      end
    end

ブロックは、ここでは評価せずに別のメソッドへ渡すため Proc オブジェクト(&block)として受けていました。

引数 mimes は respond_with を用いる際に指定しますが、block と併せて指定すると ArgumentError が raise されるようになっています。

Collector オブジェクトの生成

respond_to メソッド内で Collector オブジェクトが生成されます。

actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb
    def respond_to(*mimes, &block)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      if collector = retrieve_collector_from_mimes(mimes, &block)
        response = collector.response
        response ? response.call : render({})
      end
    end
    def retrieve_collector_from_mimes(mimes=nil, &block)
      mimes ||= collect_mimes_from_class_level
      collector = Collector.new(mimes, request.variant)
      block.call(collector) if block_given?
      format = collector.negotiate_format(request)

      if format
        _process_format(format)
        collector
      else
        raise ActionController::UnknownFormat
      end
    end

Collector オブジェクト生成時には、動的にメソッドが定義(Mime::SET が持つ Mime::Type 毎)されます。

actionpack-4.1.1/lib/abstract_controller/collector.rb
    def self.generate_method_for_mime(mime)
      sym = mime.is_a?(Symbol) ? mime : mime.to_sym
      const = sym.upcase
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{sym}(*args, &block)
          custom(Mime::#{const}, *args, &block)
        end
      RUBY
    end

    Mime::SET.each do |mime|
      generate_method_for_mime(mime)
    end
[/ruby]

<h3 id="no3">respond_to 引数ブロックの評価</h3>
<p>respond_to メソッドへ渡したブロックが、Collector オブジェクトを引数として評価されます。</p>
<h5>actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb</h5>

    def retrieve_collector_from_mimes(mimes=nil, &block)
      mimes ||= collect_mimes_from_class_level
      collector = Collector.new(mimes, request.variant)
      block.call(collector) if block_given?
      format = collector.negotiate_format(request)

      if format
        _process_format(format)
        collector
      else
        raise ActionController::UnknownFormat
      end
    end

Mime タイプ毎の処理を収集

前段でブロックが評価されることで、Collector により Mime タイプ毎の処理(ブロック)が収集されます。

respond_to do |format|
  format.html { ... }
  format.json { ... }
end

format が引数として渡された Collector オブジェクトです。動的に定義されたメソッド(html, json)へそれぞれの処理を記述したブロックを引数として渡すことで、Collector は Mime タイプ毎に処理を保持します。

Collector オブジェクトへ動的に定義された html メソッド
def html(*args, &block)
  custom(Mime::HTML, *args, &block)
end
actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb
      def custom(mime_type, &block)
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
        @responses[mime_type] ||= if block_given?
          block
        else
          VariantCollector.new(@variant)
        end
      end

Collector から response の取得

Collector からリクエストに応じた処理(response)が取得されます。

actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb
    def respond_to(*mimes, &block)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      if collector = retrieve_collector_from_mimes(mimes, &block)
        response = collector.response
        response ? response.call : render({})
      end
    end
      def response
        response = @responses.fetch(format, @responses[Mime::ALL])
        if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
          response.variant
        elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
          response
        else # `format.html{ |variant| variant.phone }` - variant block syntax
          variant_collector = VariantCollector.new(@variant)
          response.call(variant_collector) # call format block with variants collector
          variant_collector.variant
        end
      end

ここでリクエストフォーマットに応じた処理が特定されているんですね。

response の実行

response が実行されて完了です。

actionpack-4.1.1/lib/action_controller/metal/mime_responds.rb
    def respond_to(*mimes, &block)
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

      if collector = retrieve_collector_from_mimes(mimes, &block)
        response = collector.response
        response ? response.call : render({})
      end
    end

まとめ

リクエストフォーマット毎の処理を簡潔に記述できる裏側にはこのような実装があったのかという発見と共に、未だ知らない記法などを目にすることができて良い気付きになりました。
Ruby などの軽量言語は、処理の中身が知りたい時にさっとソースを閲覧できるのが良いですね。