respond_to について調べてみた
こんにちは。クラスメソッドの稲毛です。
respond_to は、Ruby on Rails のアクションメソッド内でリクエストフォーマット(html, json, etc...)に応じた処理の記述で見かけます。
respond_to do |format| format.html { ... } format.json { ... } end
どのような仕組みで動作しているのか気になっていたので少し調べてみました。
処理の流れ
おおまかな処理の流れは下記のようになっていました。
- respond_to メソッドの呼び出し
- Collector オブジェクトの生成
- respond_to 引数ブロックの評価
- Mime タイプ毎の処理を収集
- Collector から response の取得
- 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 などの軽量言語は、処理の中身が知りたい時にさっとソースを閲覧できるのが良いですね。