Action Pack: Multiview

InfoQの記事 Ruby on Rails 2.0が公式リリースをもとに、Rails2.0の新機能を順に確認しつつ、知らない機能があれば詳しく調べてしまおうというもくろみの2つ目、Action PackのMultiview。

これは、Action Pack: Resources で:format について触れた際に少しだけ出てきた、HTML以外の多様なフォーマット(XMLとか、テキストとか…)をレンダリングするためのメカニズムについての記事らしい。
以前は、テンプレートのファイル名が「アクション名.rhtml」だったのが、複数ビューのサポートにあたって汎用的なメカニズムに置き換えられ、「アクション名.フォーマット名.レンダラ名」になったと書いてある。これにより、script/generateなどで生成されるテンプレートファイル名は、「アクション名.html.erb」となっている。実際にRails2.0でブックマークアプリで生成したアプリの「views\items」を確認してみると、「edit.html.erb」など、この形式のテンプレートが存在することを確認できる。
HTML以外のでの応用例としてはAtomのレンダリングが挙げられており、「アクション名.atom.builder」と書くことで、Builderというレンダラでレンダリングできるらしい。このあたりの機能を使うための実際のコードはscaffoldで生成されたコントローラ(controllers\item_controller.rb)にも入っていて、

・${app_root}\controllers\item_controller.rb
  def index
@items = Item.find(:all)

respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @items }
end
end
こんな感じになっている。respond_toメソッドがどうなっているのか確認すると…

・gems\actionpack-2.0.2\lib\action_controller\mime_responds.rb
module InstanceMethods

def respond_to(*types, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block
block ||= lambda { |responder| types.each { |type| responder.send(type) } }
responder = Responder.new(self)
block.call(responder)
responder.respond
end
となっており、ResponderをActionControllerのサブクラスのインスタンスを渡しつつインスタンス化し、生成したResponderをブロック引数に渡してブロックをコールバックしている。Responderはどうなってるのかというと、

・gems\actionpack-2.0.2\lib\action_controller\mime_responds.rb
  class Responder #:nodoc:
def initialize(controller)
@controller = controller
@request = controller.request
@response = controller.response

@mime_type_priority = Array(Mime::Type.lookup_by_extension
(@request.parameters[:format]) || @request.accepts)

@order = []
@responses = {}
end

mime_type_priorityに、リクエストパラメータ内の:formatに対応するMime::Typeを追加している。後で出てくるが、この配列に追加したMime::Typeが優先して処理される。一方、コールバックされるブロックでは、

・${app_root}\controllers\item_controller.rb
class ItemsController < xml =""> @items }
end
end
こんな感じでformat(生成されたResponder)に対して「html」とか「xml」とかが呼び出されているわけだが、Responderにはこれら個別のフォーマットに対応したメソッドは用意されておらず、すべてmethod_missingで処理されるようになっている。上記の例ではブロックは指定されていないが、ブロックを渡すこともできるらしい。
class Responder #:nodoc:

def method_missing(symbol, &block)
mime_constant = symbol.to_s.upcase

if Mime::SET.include?(Mime.const_get(mime_constant))
custom(Mime.const_get(mime_constant), &block)
else
super
end
end
method_missingではブロックごと customメソッドに渡して…
class Responder #:nodoc:

def custom(mime_type, &block)
mime_type = mime_type.is_a?(Mime::Type) ? mime_type :
Mime::Type.lookup(mime_type.to_s)

@order <
< mime_type

@responses[mime_type] = Proc.new do
@response.template.template_format = mime_type.to_sym
@response.content_type = mime_type.to_s
block_given? ? block.call :
@controller.send(:render,
:action => @controller.action_name)
end
end
レスポンスレンダリング用の設定をした上で、ProcオブジェクトをMime::Typeに対応づけ、ハッシュに保存する。Procの内容はというと、ブロックが渡されていればブロックを呼び出し、いなければアクションを指定しつつコントローラのrenderメソッドを呼び出す、というもの。最後にrespond_toから以下のメソッドが呼び出され、この中で先ほど保存したProcが呼び出される。
class Responder #:nodoc:

def respond
for priority in @mime_type_priority
if priority == Mime::ALL
@responses[@order.first].call
return
else
if @responses[priority]
@responses[priority].call
return # mime type match found, be happy and return
end
end
end

if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
else
@controller.send :head, :not_acceptable
end
end
end
という感じで、ざっくり挙動がわかったところでAction PackのMultiviewについてはひとまず終了。renderあたりの細かい挙動は、別途とりあげます。

関連リンク:
Rails2.0リリース
Rails2.0のscaffoldは前とだいぶ違うらしい
Migration
Rails2.0でブックマークアプリ
Rails2.0の変更点
Action Pack: Resources
Action Pack: Record identification

コメント