はじめに
この記事はRuby on Rails Advent Calendar 2022の14日目の記事です。
概要
この記事ではRailsのrender
メソッドを読み、内部で何が起きているのかを理解していきます。
動機
私は個人でAlbaというJSONシリアライザを開発しています。その際、Railsの機能に合わせて以下のような動作をさせるようにしました。
render json: FooResource.new(foo)
これは
render json: FooResource.new(foo).serialize
と同じ挙動になっています。
なぜこうなるのか?をまとめるのがこの記事の目的です。そしてその目的のため、Railsのソースコードを読んでいきます。
注意
以下のコードは全てRails7.0.4時点のものとなります。
本編
renderメソッドの定義箇所を特定する
コントローラ内に以下のスニペットを置くことでrender
メソッドの定義箇所がわかります。
pp method(:render).source_location
私は適当にrails new
したアプリに空っぽのコントローラを作ってそこに書きました。その結果、
がヒットしました。ここでは22行目でsuper
を読んでいるので、そちらに処理が移っているようです。ファイル名からしても(計測用モジュールらしいので)、super
先を見に行くべきでしょう。
ここでは先ほどのスニペットを修正してsuper
先のメソッドの定義箇所を調べます。
pp method(:render).super_method.source_location
その結果、
が出ましたが、これも内容がないのでもう一回super
を調べます。
pp method(:render).super_method.super_method.source_location
そうしたら
が出ました。ここにはsuper
がないので、このメソッドを見ていきましょう。
render
メソッドの中身を調べる
上記メソッドの内容は以下の通りです。
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
if options[:html]
_set_html_content_type
else
_set_rendered_content_type rendered_format
end
_set_vary_header
self.response_body = rendered_body
end
_normalize_render
がrender
に渡されたオプションを処理する箇所のようです。
_normalize_args
が中心的な処理のようです。
render json: something
において、このメソッドのaction
引数に渡されるのはjson: something
であり、Hash
であるのでaction
、すなわちjson: something
が返ります。返り値は_normalize_render
内のoptions
変数に代入され、_process_variant
に渡されます。
_process_variant
は空です。これまでの継承チェーンのどこかに同名のメソッドがあるでしょうか。
ありました。条件を満たしていれば:variant
のキーに対して値をセットするようです。
次の_normalize_options
も同様で、
で:status
などをoptions
に追加しています。
コメントに「render_to_body
に委譲する」と書いてあります。先ほどまでで作られたoptions
がそれに渡されているようです。それも見てみましょう。
なんと中身は空です。ということは、これまで見てきたファイルの中の同名のメソッドがあるはずです。再びRailsアプリケーションにスニペットを仕込んでみましょう。
pp method(:render_to_body).source_location
すると、
が出てきます。このメソッドは_render_to_body_with_renderer
を呼んでいます。
_renderers
という変数が出てきました。これはどこでセットされているのでしょうか。
renderer
の概念
このRenderers
というファイルに何かありそうです。このファイルを_renderers
で検索すると、
と
で代入しているのが見つかります。コメントを読むに怪しそうなのは前者です。All
というモジュールがinclude
されるとこのコードが呼ばれるようです。また、代入されるオブジェクトはRENDERERS
という定数になっています。All
でRailsのコードベースを検索すると、
が出てくるので、ActionController::Base
はRenderers::All
をinclude
していることがわかりました。では、RENDERERS
には何が入っているのでしょうか。まず、
から、RENDERERS
はSet
です。そして、
から、add
というクラスメソッドを呼ぶとRENDERERS
にオブジェクトが追加されるようです。このadd
は同じファイル内でも使われていて、
でやっとお目当てのJSONレンダーにたどり着きました。
JSONレンダリングの中身
当該のadd
の中身は以下です。
add :json do |json, options|
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
if media_type.nil? || media_type == Mime[:json]
self.content_type = Mime[:js]
end
"/**/#{options[:callback]}(#{json})"
else
self.content_type = Mime[:json] if media_type.nil?
json
end
end
add
内の1行目で引数が文字列でなければto_json(options)
を呼ぶようになっています。これが今回私がAlbaの開発で探していた行でした。ここでのoptions
がくせ者で、これまで見てきた:status
のようなキーを含んだハッシュとなっています。必ず渡されるのと、Ruby3.0以降のキーワード引数の仕様変更の影響を受けるらしく、そのため
のような書き方をする必要がありました。
add
からrender
に戻る
さて、add
の処理に戻ると、渡したブロックの中身でメソッドが定義されます。このメソッドは
で_render_to_body_with_renderer
から呼ばれます。この部分に到達するには、
で_renderers_に含まれる
:jsonのシンボルが
optionsのキーに含まれている必要があり、先ほどまでの話と合致しています。ここで返された値は
render`メソッドまで戻り、
でself.response_body
に格納されます。まだ続きがありそうですが、長くなったので今日はここまでとします。
まとめ
当初の想定よりだいぶガッツリとしたコードリーディングになってしまいました。Railsにおいては一見単純に見える処理もだいぶ複雑な工程を経て処理されていることがわかりますね。
今回は特にそうだったのですが、Railsのコードリーディングにおいてはgrepするだけでは目的を達成できないことが多いです。source_location
などを駆使する必要があるのですが、慣れれば結構楽しいので皆さんも気になる箇所を読んでみてはいかがでしょうか。
宣伝
コードを読むのが好きなあなたに、こんなコミュニティはいかがでしょうか。
主にgemのコードを読む会です。メインの読み手は私が務め、それに参加者の方がガヤを入れる形式となります。次回開催は未定ですが、よければメンバーになってみてください。