7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsのメソッド"redirect_to"がどう定義されているのか読解してみた

Posted at

railsメソッドの定義を解釈することで、rubyらしい書き方を学ぼう企画第二弾です。
今回のテーマはredirect_to

redirect_toの定義

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

1行目

def redirect_to(options = {}, response_status = {})

引数を2つ取ります。
いずれも、引数が渡されなかった場合は空のハッシュが代入されます。

2行目

raise ActionControllerError.new("Cannot redirect to nil!") unless options

opsionsが空のときにraiseでエラー発生。
ちなみにrubyはfalseとnil以外はすべてtrueとして扱うので空のハッシュ{}ではエラー発生しません。
ユーザがあえてnil/falseを代入した場合にエラー発生します。

3行目

raise AbstractController::DoubleRenderError if response_body

すでにHTTPレスポンスを受け取っている場合はエラー発生します。
ここでresponse_bodyが代入される仕組みは、わかっていませんが、同時に複数のHTTP通信がやりとりされたときにエラーを返すようです。

4行目

self.status = _extract_redirect_to_status(options, response_status)

今回も新しいメソッドが出てきましたね。

_extract_redirect_to_statusメソッド

定義

# Fileactionpack/lib/action_controller/metal/redirecting.rb, line 117
def _extract_redirect_to_status(options, response_status)
  if options.is_a?(Hash) && options.key?(:status)
    Rack::Utils.status_code(options.delete(:status))
  elsif response_status.key?(:status)
    Rack::Utils.status_code(response_status[:status])
  else
    302
  end
end

2行目

if options.is_a?(Hash) && options.key?(:status)

if条件の1項目のis_a?メソッドはレシーバ(ここでは"options")の型がカッコ内に書かれた型(ここでは"ハッシュ")と一致すればtrueを返すメソッド。
※モデル名、例えばBookとかUserとかをカッコに入れると、そのモデルに基づいて生成されたインスタンスであるかどうかの判定もしてくれます。便利!)

if条件の2項目のkey?はレシーバが"カッコ内に記述された名前のキー"を持つ場合に真を返すメソッド。

つまりredirect_toに渡された引数がハッシュで、かつキーstatusを持っている場合にif文の中身が実行されます。
冒頭に上げたRailsドキュメントにもあるように

redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }

のようにstatusはredirect_toメソッドを使うプログラマーが設定するパラメータです。
読解をすすめる中で、どんな役割を持つパラメータなのかわかりますね。

3行目

Rack::Utils.status_code(options.delete(:status))

RackはRailsアプリケーションとアプリケーションサーバの仲介をする機能のプログラム群です。
(ミドルウェアと呼ばれる)
status_codeはそのうちUtilsというライブラリで定義されるメソッドです。

deleteメソッドによりoptionsのキーstatusに代入された値がstatus_codeメソッドに引き渡されます。
※ここではキーの値を削除するため、ではなくキーの値を返すためにこのメソッドが使用されていると解釈しました。

status_codeメソッド

def status_code(status)
  if status.is_a?(Symbol)
    SYMBOL_TO_STATUS_CODE[status] || 500
  else
    status.to_i
  end
end

このメソッドは単にstatusの値を表示するメソッドですね。
Symbolで渡されている場合(2行目のis_aで判定)のときのみSYMBOL_TO_STATUS_CODEというRack::Utils内で定義されている変換用のハッシュが用いられます。

定義自体は

SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
  [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]

のようになっています。
このコードによって同じくRack::Utilsに定義されているハッシュHTTP_STATUS_CODESを{:HTTPステータス名 => HTTPステータスコード}というハッシュ配列に変換します。

具体的には

 pry(main)> Rack::Utils::SYMBOL_TO_STATUS_CODE
=> {:continue=>100,
 :switching_protocols=>101,
 :processing=>102,
 :early_hints=>103,
 :ok=>200,
 :created=>201,
 :accepted=>202,
 :non_authoritative_information=>203,
 :no_content=>204,
 :reset_content=>205,
 :partial_content=>206,
 :multi_status=>207,
 :already_reported=>208,
 :im_used=>226,
 :multiple_choices=>300,
 :moved_permanently=>301,
 :found=>302,
 :see_other=>303,
 :not_modified=>304,
 :use_proxy=>305,
 :"(unused)"=>306,
 :temporary_redirect=>307,
 :permanent_redirect=>308,
:bad_request=>400,
 :unauthorized=>401,
 :payment_required=>402,
 :forbidden=>403,
 :not_found=>404,
 :method_not_allowed=>405,
 :not_acceptable=>406,
 :proxy_authentication_required=>407,
 :request_timeout=>408,
 :conflict=>409,
 :gone=>410,
 :length_required=>411,
 :precondition_failed=>412,
 :payload_too_large=>413,
 :uri_too_long=>414,
 :unsupported_media_type=>415,
 :range_not_satisfiable=>416,
 :expectation_failed=>417,
 :misdirected_request=>421,
 :unprocessable_entity=>422,
 :locked=>423,
 :failed_dependency=>424,
 :too_early=>425,
 :upgrade_required=>426,
 :precondition_required=>428,
 :too_many_requests=>429,
 :request_header_fields_too_large=>431,
 :unavailable_for_legal_reasons=>451,
 :internal_server_error=>500,
 :not_implemented=>501,
 :bad_gateway=>502,
 :service_unavailable=>503,
 :gateway_timeout=>504,
 :http_version_not_supported=>505,
 :variant_also_negotiates=>506,
 :insufficient_storage=>507,
 :loop_detected=>508,
 :bandwidth_limit_exceeded=>509,
 :not_extended=>510,
 :network_authentication_required=>511}
# Railsコンソール上で実行。

つまり、status_codeの定義に戻ると

SYMBOL_TO_STATUS_CODE[status] || 500

においてstatus: :not_foundのときは"404"が返されます。
該当するものがない場合は"500"を返す。

symbolではなくoptions[:status]に"404"などの値が表示されている場合は4~5行目によってinteger形式でそのままの値が返されますね。

else
  status.to_i
end

_extract_redirect_to_statusメソッド

(再掲)

# Fileactionpack/lib/action_controller/metal/redirecting.rb, line 117
def _extract_redirect_to_status(options, response_status)
  if options.is_a?(Hash) && options.key?(:status)
    Rack::Utils.status_code(options.delete(:status))
  elsif response_status.key?(:status)
    Rack::Utils.status_code(response_status[:status])
  else
    302
  end
end

ここまで、3行目で何をやっているか読解しました。

4行目

elsif response_status.key?(:status)

key?メソッドにより、引数response_statusにキーstatusがあるときにこの条件文はtrueとなります。
実行されるメソッドは先程と同じですね。

optionsかresponse_statusどちらでstatus値が引き渡されていたとしても、正しくHTTPステータスを返せるようにするためのif分岐のようです。

なお返すHTTPステータスが渡されていない場合は
6~7行目のelse文により302が返されます。
("リダイレクト処理"を意味します。)

redirect_to

(再掲)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

4行目でユーザ側に返すHTTPステータスコードを決めました。

5行目

self.location = _compute_redirect_to_location(request, options)

新しいrailsメソッドが登場しました。
ここで代入されているrequestというのはコントローラ上で毎回、生成されるオブジェクトのことです。
ユーザ側からrailsアプリ側へと出されたHTTPリクエストの各パラメータが格納されています。

具体的にどんなパラメータがあるかどうかは
Railsガイドにまとめられています。

request.hostのように値を取り出すことができます。

定義自体はActionDispatch::Requestモデル上でなされています。
このモデルで用いることのできるメソッドはこちらにリスト化されています。

_compute_redirect_to_locationメソッド

定義

# File actionpack/lib/action_controller/metal/redirecting.rb, line 96
    def _compute_redirect_to_location(request, options) #:nodoc:
      case options
      # The scheme name consist of a letter followed by any combination of
      # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
      # characters; and is terminated by a colon (":").
      # See https://tools.ietf.org/html/rfc3986#section-3.1
      # The protocol relative scheme starts with a double slash "//".
      when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
        options
      when String
        request.protocol + request.host_with_port + options
      when Proc
        _compute_redirect_to_location request, instance_eval(&options)
      else
        url_for(options)
      end.delete("\00\\r\n")
    end

このメソッドではリダイレクト先のURLが生成されます。
case文による条件分岐はopsionsの内容によってなされます。
※この項では行数はコメントを含めてカウントします。

9~10行目

when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
  options

http://のように先頭に○○://とつく文字列のときに、この条件がtrueとなります。
URLの体をとっていると判定されるので、そのままoptionsの値が返されます。

11~12行目

when String
  request.protocol + request.host_with_port + options

optionsが文字列のときに呼び出されます。
先程、紹介したユーザ側からのリクエスト情報が格納されたrequestオブジェクトをもとにURLが生成されます。
protocolにはhttp://やhttps://が格納されています。
host_with_portにより://以降、ホストが生成されます。
最後にoptionsが付与されます。

host_with_portメソッドはrequestオブジェクトに格納されているプロパティ=hostとport_stringを呼び出し、文字列として結合するシンプルなメソッドです。

# File actionpack/lib/action_dispatch/http/url.rb, line 250
def host_with_port
  "#{host}#{port_string}"
end

13~14行目

when Proc
  _compute_redirect_to_location request, instance_eval(&options)

Procとは

ブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。
(https://docs.ruby-lang.org/ja/latest/class/Proc.html)[https://docs.ruby-lang.org/ja/latest/class/Proc.html]

redirect_toはブロック形式で用いることができ、ブロックが与えられたときにこの分岐に入るという認識でOKです。
※ブロックについては前回のlink_toに関する記事で触れています

例えば

get 'jokes/:number', to: redirect { |params, request|
  path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
  "http://#{request.host_with_port}/#{path}"
  }

のような記述ができます。

ブロックが呼び出されると再帰的に_compute_redirect_to_locationメソッドが呼び出されます。
初学者向けに説明すると、再帰とはあるメソッドの中で、同じメソッドを呼び出すことです。
例えば

def method
  method
end

のような形です。
(この場合は無限ループになってしまいます。普通はif分岐などで無限ループしないように書かれます)

(再掲)

\_compute_redirect_to_location request, instance_eval(&options)

_compute_redirect_to_location(request, options)メソッドの第二項にはinstance_eval(&options)が代入されています。

optionsの前に&とついていますが、これはブロックを代入する、という意味ですね。
instance_evalはブロックの中のコードを実行したときの出力を返すメソッドです。

先程の例で考えてみましょう
(再掲)

get 'jokes/:number', to: redirect { |params, request|
  path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
  "http://#{request.host_with_port}/#{path}"
  }

この場合だと、2~3行目のコードが実行され、結果として3行目のhttp://から始まるURLが文字列として返されます。
つまり
_compute_redirect_to_locationメソッドにURLがもろに代入されます。
case文が呼び出され、先述の9~10行目の条件に分岐します。
(再掲)

when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
  options

15~16行目

else
  url_for(options)

これまでの条件をまとめると
http://~といった具体的なURL、ファイル名を指す文字列、ブロックについて分岐されてきたが、いずれにも該当しない場合はこのelse文の内容が実行されることになります。

ここで出てくるのはlink_to内でも用いられていたurl_forメソッドです。

def url_for(options)
    if options[:only_path]
      path_for options
    else
      full_url_for options
    end
end
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080'
# => 'http://somehost.org:8080/tasks/testing'
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true
# => '/tasks/testing#ok'
url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true
# => 'http://somehost.org/tasks/testing/'
url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33'
# => 'http://somehost.org/tasks/testing?number=33'

(引用)
のようにURLを生成します。

17行目

end.delete("\00\\r\n")

case文の最後にdeleteが呼び出されます。

文字列に対して呼び出されているのでこれまで出てきたdeleteとは挙動が変わります。
Rubyリファレンスによると、

delete(*strs) -> String
strs に含まれる文字を取り除いた文字列を生成して返します。

同リファレンスにはstrsの書式についても参照記事が掲載されています。
重要度が低く、理解できていない部分があるので、断定はしませんが、
\r、\nはそれぞれ「復帰」「改行」を表す特殊文字ですので、urlに紛れ込んだこれらの文字を削除するような意味合いがあると推定しています。
となると\00\が何かよくわかりませんし、もしかすると\0, 0, \, r, \nと区切ることもできそうです。

ここまでが_compute_redirect_to_locationメソッドでした。

redirect_to

(再掲)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

4行目でリダイレクト後にユーザに返すHTTPステータスコードを決めました。
5行目でリダイレクト先のURLを決めました。

6行目

self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"

ではユーザ側に返すhtmlデータを決めます。
実際にはlocationの値に基づいて、リダイレクト処理が行われるため、このhtmlがユーザブラウザ上で表示されることはありません。

ここで、self.〜に格納された値はユーザ側のブラウザにHTTPレスポンスとして引き渡されます。
HTTPヘッダのうちlocationプロパティ(属性)のURLを見て、ユーザのブラウザは再度リクエストを行います。

ブラウザの処理の仕組みはこちらを参照。

まとめ

(再掲)

# File actionpack/lib/action_controller/metal/redirecting.rb, line 56
def redirect_to(options = {}, response_status = {})
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
  raise AbstractController::DoubleRenderError if response_body

  self.status        = _extract_redirect_to_status(options, response_status)
  self.location      = _compute_redirect_to_location(request, options)
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end

redirect_toは呼び出されると

  1. optionsにnilが含まれていればエラー
  2. すでにresponseが生成されていればエラー
  3. ユーザ側に返すHTTPレスポンスのステータス(200:成功、404:Not Foundエラーなど)をレスポンスオブジェクトに格納する
  4. リダイレクト先のURLをレスポンスオブジェクトに格納する
  5. HTMLデータをレスポンスオブジェクトに格納する(ただし基本的には表示されることはない)

感想

このメソッドの中で処理を行っている、というよりクライアント(ユーザが使用しているブラウザ)に実際の処理は任せる前提で、そのために必要なレスポンスデータを生成する、という役割だとわかりました。

Railsのアプリケーションやミドルウェアが行っている処理への理解が深まれば、より複雑な処理も実装できるようになりそうですね。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?