はじめに
この記事は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のコードを読む会です。メインの読み手は私が務め、それに参加者の方がガヤを入れる形式となります。次回開催は未定ですが、よければメンバーになってみてください。