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

railsメソッドの定義を解釈することで、rubyらしい書き方を学ぼう企画第二弾です。
今回のテーマは[redirect_to](https://api.rubyonrails.org/v5.1.7/classes/ActionController/Redirecting.html#method-i-redirect_to)

#redirect_toの定義

```ruby
# 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行目
```ruby
def redirect_to(options = {}, response_status = {})
```
引数を２つ取ります。
いずれも、引数が渡されなかった場合は空のハッシュが代入されます。

##2行目

```ruby
raise ActionControllerError.new("Cannot redirect to nil!") unless options
```
opsionsが空のときにraiseでエラー発生。
ちなみにrubyはfalseとnil以外はすべてtrueとして扱うので空のハッシュ{}ではエラー発生しません。
ユーザがあえてnil/falseを代入した場合にエラー発生します。

##3行目
```ruby
raise AbstractController::DoubleRenderError if response_body
```
すでにHTTPレスポンスを受け取っている場合はエラー発生します。
ここでresponse_bodyが代入される仕組みは、わかっていませんが、同時に複数のHTTP通信がやりとりされたときにエラーを返すようです。
## 4行目
```ruby
self.status = _extract_redirect_to_status(options, response_status)
```
今回も新しいメソッドが出てきましたね。

#\_extract_redirect_to_statusメソッド
[定義](https://apidock.com/rails/ActionController/Redirecting/_extract_redirect_to_status)

```ruby
#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行目

```ruby
if options.is_a?(Hash) && options.key?(:status)
```
if条件の１項目の[is_a？メソッド](https://docs.ruby-lang.org/ja/latest/method/Object/i/is_a=3f.html)はレシーバ（ここでは"options"）の型がカッコ内に書かれた型(ここでは"ハッシュ")と一致すればtrueを返すメソッド。
※モデル名、例えばBookとかUserとかをカッコに入れると、そのモデルに基づいて生成されたインスタンスであるかどうかの判定もしてくれます。便利！)

if条件の2項目の[key?](https://docs.ruby-lang.org/ja/latest/method/Hash/i/keys.html)はレシーバが"カッコ内に記述された名前のキー"を持つ場合に真を返すメソッド。

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

```ruby
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行目
```ruby
Rack::Utils.status_code(options.delete(:status))
```
RackはRailsアプリケーションとアプリケーションサーバの仲介をする機能のプログラム群です。
(ミドルウェアと呼ばれる)
[status_code](https://www.rubydoc.info/gems/rack/1.6.4/Rack%2FUtils.status_code)はそのうちUtilsというライブラリで定義されるメソッドです。

[deleteメソッド](https://docs.ruby-lang.org/ja/latest/method/Hash/i/delete.html)によりoptionsのキーstatusに代入された値がstatus_codeメソッドに引き渡されます。
※ここではキーの値を削除するため、ではなくキーの値を返すためにこのメソッドが使用されていると解釈しました。

#status_codeメソッド
```ruby
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](https://github.com/rack/rack/blob/cd5d902ac801f0bebeeb5d8dc534f717c9822b9b/lib/rack/utils.rb#L581)というRack::Utils内で定義されている変換用のハッシュが用いられます。

定義自体は

```ruby
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
  [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]
```
のようになっています。
このコードによって同じくRack::Utilsに定義されているハッシュ[HTTP_STATUS_CODES](https://github.com/rack/rack/blob/cd5d902ac801f0bebeeb5d8dc534f717c9822b9b/lib/rack/utils.rb#L512)を{:HTTPステータス名 => HTTPステータスコード}というハッシュ配列に変換します。

具体的には

```ruby
 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の定義に戻ると

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

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

```ruby
else
  status.to_i
end
```

#\_extract_redirect_to_statusメソッド
(再掲)

```ruby
#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行目

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

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

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

#redirect_to
(再掲)

```ruby
# 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行目

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

具体的にどんなパラメータがあるかどうかは
[Railsガイド](https://railsguides.jp/action_controller_overview.html#request%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%A8response%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88)にまとめられています。

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

定義自体はActionDispatch::Requestモデル上でなされています。
このモデルで用いることのできるメソッドは[こちら](https://api.rubyonrails.org/classes/ActionDispatch/Request.html)にリスト化されています。

# \_compute_redirect_to_locationメソッド
[定義](https://apidock.com/rails/ActionController/Redirecting/_compute_redirect_to_location)

```ruby
# 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行目

```ruby
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/
  options
```
http://のように先頭に○○://とつく文字列のときに、この条件がtrueとなります。
URLの体をとっていると判定されるので、そのままoptionsの値が返されます。

##11~12行目

```ruby
when String
  request.protocol + request.host_with_port + options
```
optionsが文字列のときに呼び出されます。
先程、紹介したユーザ側からのリクエスト情報が格納されたrequestオブジェクトをもとにURLが生成されます。
protocolにはhttp://やhttps://が格納されています。
[host_with_port](https://api.rubyonrails.org/v5.1.7/classes/ActionDispatch/Http/URL.html#method-i-host_with_port)により://以降、ホストが生成されます。
最後にoptionsが付与されます。

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

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

##13~14行目

```ruby
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に関する記事で触れています

例えば

```ruby
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メソッドが呼び出されます。
初学者向けに説明すると、再帰とはあるメソッドの中で、同じメソッドを呼び出すことです。
例えば

```ruby
def method
  method
end
```
のような形です。
（この場合は無限ループになってしまいます。普通はif分岐などで無限ループしないように書かれます）

(再掲)

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

optionsの前に&とついていますが、これはブロックを代入する、という意味ですね。
[instance_eval](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/instance_eval.html)はブロックの中のコードを実行したときの出力を返すメソッドです。

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

```ruby
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行目の条件に分岐します。
(再掲)

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

##15~16行目

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

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

```ruby
def url_for(options)
    if options[:only_path]
      path_for options
    else
      full_url_for options
    end
end
```

```ruby
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'
```
([引用](https://www.rubydoc.info/docs/rails/3.2.8/ActionDispatch%2FRouting%2FUrlFor:url_for))
のようにURLを生成します。


##17行目
```ruby
end.delete("\00\\r\n")
```
case文の最後に[delete](https://docs.ruby-lang.org/ja/latest/method/String/i/delete.html)が呼び出されます。

文字列に対して呼び出されているのでこれまで出てきたdeleteとは挙動が変わります。
Rubyリファレンスによると、
>delete(*strs) -> String
>strs に含まれる文字を取り除いた文字列を生成して返します。

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

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

#redirect_to
(再掲)

```ruby
# 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行目

```ruby
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を見て、ユーザのブラウザは再度リクエストを行います。

ブラウザの処理の仕組みは[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/Redirections)を参照。


#まとめ
(再掲)

```ruby
# 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のアプリケーションやミドルウェアが行っている処理への理解が深まれば、より複雑な処理も実装できるようになりそうですね。