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

優れたコードを読むことがエンジニアとして成長する促進剤になる、と職場のエリートエンジニアの方に教わったので早速、実践！
ついでに共有してみます。

良質なコードを学ぶことで可読性が高く、バグの少ないコードを書けるようになるかと思いますので、ぜひ皆さんも参考にしてみてください。

手始めにlink_toから。

# link_toの定義
```
def link_to(name = nil, options = nil, html_options = nil, &block)
  html_options, options, name = options, name, block if block_given?
  options ||= {}

  html_options = convert_options_to_data_attributes(options, html_options)

  url = url_for(options)
  html_options["href"] ||= url

  content_tag("a", name || url, html_options, &block)
end
```
[Ruby on Rails公式API](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)より、（ブロック=do~endを用いない場合は）nameに表示する文字列、optionsにはurl情報、html_optionsにはclassやrelなどHTMLのaタグに設定したい値が引き渡されます。

optionsとhtml_optionsはハッシュ形式で渡すことが前提となります。

##早速、一行ずつ解釈を加えていきます。
```
def link_to(name = nil, options = nil, html_options = nil, &block)
```
引数の定義にある=nilの記述は、何も値が代入されていないとき、当該の引数にnilを代入する、という意味。
第４引数にある&blockは"ブロック"を受け取るための記述。
[こちらの記事](https://yakst.com/ja/posts/1634)を読むと、ブロックとは***引数として値ではなく、コードの塊を渡す***際に用いるもので、***引き渡されたコードの塊をブロックと呼ぶ***ようです。

link_to do〜endと記述する際の〜がここに代入される。
##次の行
```
html_options, options, name = options, name, block if block_given?
```
[block_given?(Rubyリファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/Kernel/m/block_given=3f.html)
リファレンスマニュアルより、link_toメソッドにブロックが引き渡されているときにblock_given?はtrueとなりif文の中身が実行されます
つまり[多重代入（Rubyリファレンス）](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html#multiassign)される。

例えばblockを用いる場合は[Ruby on Rails公式API](https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)より、

```
link_to(options = {}, html_options = {}) do
  # name
end
```
と記述されます
この場合はlink_to定義の1行目より、本来は表示名称であるnameにoptions(url情報)が、url情報であるoptionsにclassやidを定義するhtml_optionsが引き渡されてしまいます

そこでメソッド使用者の意図した通りに動くように、メソッドの定義通りに代入し直してやるのがこの行の役割。
（例えばurl情報が代入されてしまったnameの値を、optionsに代入しなおしている）

##次
```
options ||= {}
```
||=は左辺がfalseもしくは定義されていないときに右辺の値を代入する、という記述。
1行目で、引数に値が渡されていないときにはoptionsにnilを代入するため、optionsの中身はnil、つまりfalseとなります
このときにoptinsには{}（空のハッシュ）が代入されます

```
html_options = convert_options_to_data_attributes(options, html_options)
```
convert_options_to_data_attributesというメソッドが呼び出されています。

#convert_options_to_data_attributesメソッドの読解
このメソッドの定義は[こちらのリンク](https://apidock.com/rails/ActionView/Helpers/UrlHelper/convert_options_to_data_attributes)

```
def convert_options_to_data_attributes(options, html_options)
  if html_options
    html_options = html_options.stringify_keys
    html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method".freeze)

    add_method_to_attributes!(html_options, method) if method

    html_options
  else
    link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {}
  end
end
```
convert_options_to_data_attributesの読解をしてみましょう。

html_optionsが存在する場合は3行目が実行されます。

```
html_options = html_options.stringify_keys
```
[stringfy_keys](https://apidock.com/rails/v4.0.2/Hash/stringify_keys)はハッシュをシンボル形式から文字列形式に変換するrailsメソッド。

##4行目
```
html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)
```
については要するにoptionsもしくはhtml_optionsにremote: trueという設定があれば文字列形式でキーdata-remoteに対してtrueが代入されます

なぜそうなるかは、railsメソッドのlink_to_remote_options?の内容を理解できればわかるかと思います。
else文の解釈のところで詳しく述べるので先にそちらを参照してみてください。

##5行目
```
method = html_options.delete("method".freeze)
```
ハッシュに対する[delete](https://docs.ruby-lang.org/ja/latest/method/Hash/i/delete.html)は引数の値と一致するキーを削除→削除したキーの値を返します
引数はmethod".freezeとなっています。
[freeze](https://docs.ruby-lang.org/ja/latest/method/Object/i/freeze.html)によってdeleteの１つ目の機能である削除は行われず、2つ目の機能、すなわちmethodの値のみを返します。

したがって変数methodにhtml_optionsのキーmethodの値が代入されます。
(HTMLメソッドの設定値)
この書き方は面白いですね。

##6行目
```
add_method_to_attributes!(html_options, method) if method
```

さらにmethodの値が設定されている場合は[add_method_to_attributes!メソッド](https://apidock.com/rails/ActionView/Helpers/UrlHelper/add_method_to_attributes%21)を呼び出します。
!マークがついているので[破壊的メソッド](https://techacademy.jp/magazine/19934)。

###add_method_to_attributes!メソッド
add_method_to_attributes!メソッドは下記のように定義されています。

```
def add_method_to_attributes!(html_options, method)
  if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
    if html_options["rel"].blank?
      html_options["rel"] = "nofollow"
    else
      html_options["rel"] = "#{html_options["rel"]} nofollow"
    end
  end
  html_options["data-method"] = method
end
```
#add_method_to_attributes!メソッドの読解
##add_method_to_attributes!メソッド2行目。
ifの条件式はAND条件。
AND条件の１項目は[method_not_get_method?メソッド](https://github.com/rails/rails/blob/6-1-stable/actionview/lib/action_view/helpers/url_helper.rb#L709-L712)の返り値。

このメソッドの定義式は省略しますが、その名の通り、get以外のメソッド（deleteとか）の場合にtrueを返します。
（リーダブルコードにも載っている命名の仕方ですね！）

AND条件の2項目は[Rubyリファレンスの!の項の3つ目](https://docs.ruby-lang.org/ja/latest/doc/symref.html#ex)。
html_optionsのキーrelの値がnofollowでない場合はtrue。

##add_method_to_attributes!メソッド3〜7行目
先述のAND条件が成立した場合に実行。

```
if html_options["rel"].blank?
  html_options["rel"] = "nofollow"
else
  html_options["rel"] = "#{html_options["rel"]} nofollow"
end
```
一気に行きましょう。
html_optionsのキーrelの値が空のときはnofollowという文字列を代入。
html_optionsのキーrelの値に何かしらの値が含まれているときは、その設定値に加えてnofollowという文字列を追加。

2行目のif文と組み合わせて解釈すると、get以外のメソッドのときはrelに値が設定されていようと、なかろうとrel="nofollow"となります。

ちなみにnofollowの意味は["リンクをクロールの対象から外す"](https://www.seohacks.net/blog/column/1140/#:~:text=rel%3D%E2%80%9Dnofollow%E2%80%9D%E3%81%AE%E6%84%8F%E5%91%B3,%E3%81%AA%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%A8%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%A7%E3%81%99%E3%81%AD%E3%80%82)だそうです。

なぜget以外のメソッドにnofollowを付与すると良いかはよく分からず、推察しかできていないので省略します。
またrel自体の使いみちについては[こちら](https://saruwakakun.com/html-css/basic/link-rel#:~:text=%E2%86%91%20rel%E3%81%AF%E3%80%8Crelation%EF%BC%88%E9%96%A2%E4%BF%82,%E3%81%A7%E7%A4%BA%E3%81%99%E3%82%8F%E3%81%91%E3%81%A7%E3%81%99%E3%81%AD%E3%80%82)を参照

##add_method_to_attributes!メソッド8行目。
```
html_options["data-method"] = method
```
逆にgetメソッドと指定されている場合は、2行目のif文によってelse文に分岐。
上記のコードが実行されます。

html_optionsのキー"data-method"にHTTPメソッドの種類が代入されます。
"data-"となっているのでカスタム属性として定義されるということですね。

ようやくadd_method_to_attributes!メソッドの解釈が終わりました。

#convert_options_to_data_attributesの読解に戻ります
(再掲)

```
def convert_options_to_data_attributes(options, html_options)
  if html_options
    html_options = html_options.stringify_keys
    html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)

    method = html_options.delete("method".freeze)

    add_method_to_attributes!(html_options, method) if method

    html_options
  else
    link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {}
  end
end
```
##7行目。
4、６行目で２つの値を代入したハッシュhtml_optionsを返します。

##8~9行目
html_optionsに値が渡されていない場合は

```
link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {}
```
が実行される。

三項演算子ですね。
まずは[link_to_remote_options?](https://apidock.com/rails/ActionView/Helpers/UrlHelper/link_to_remote_options%3F)が呼び出される。

```
def link_to_remote_options?(options)
  if options.is_a?(Hash)
    options.delete("remote".freeze) || options.delete(:remote)
  end
end
```
#link_to_remote_options?メソッド
optionsの値がハッシュであるとき、if文の中身が実行されます

ただしremoteの設定があったとしてもremote: falseと定義されていた場合は、1項目の値はfalseとなります。
そこで2項目が呼び出され、今度はfreezeの記述がないためremoteの設定は削除されます

要するにlink_to_remote_options?により、remoteの指定がない OR falseの場合はfalseが、remoteの値がtrueの場合はtrueが返されます。

#再度、convert_options_to_data_attributesメソッドのelse文の読解
三項演算子により、１項目でtrueが返される、すなわちremote: trueの設定がされている場合は、シンボル"data-remote" => trueがconvert_options_to_data_attributesの返り値となります。

convert_options_to_data_attributes内のif文のいずれであってもhtml_optionsのキーdata-remoteの値が設定される理由としては、link_toメソッドの引数、optionsとhtml_optionsいずれにremoteの設定値が渡された場合でも、カスタム属性remoteの値をhtml表示してやるためと思われます。

link_toへの値の引き渡しの書き方パターンについてはここでは省略します。

#link_toに戻ります
(再掲)

```
def link_to(name = nil, options = nil, html_options = nil, &block)
  html_options, options, name = options, name, block if block_given?
  options ||= {}

  html_options = convert_options_to_data_attributes(options, html_options)

  url = url_for(options)
  html_options["href"] ||= url

  content_tag("a", name || url, html_options, &block)
end
```
4行目までで、html_optionsの値が整形されました。

##次は5行目。
```
url = url_for(options)
```
[url_forメソッド](https://qiita.com/ozipi/items/6df659a34f12b6b753ce)が呼び出されます。

```
def url_for(options)
    if options[:only_path]
      path_for options
    else
      full_url_for options
    end
end
```
path_forが相対パスを出力するメソッド、full_url_forが絶対パスを出力するメソッド。
optionsのキーonly_pathの値がtrueの時に相対パスを出力する。

##link_toの6行目
```
html_options["href"] ||= url
```
先程、生成したパスをhtml_optionsのキーhrefに代入。
ここからわかることは、link_toの第三因数にhrefをキーとしてやれば強制的にURLを指定することもできるということですね。

使いどきがあるかどうかはともかく…

##最後7行目
```
content_tag("a", name || url, html_options, &block)
```
[content_tagはviewヘルパー]。(https://apidock.com/rails/ActionView/Helpers/TagHelper/content_tag)
htmlタグを出力します。

第一引数がhtmlタグの種類、この場合は<a>ですね。

第二引数は表示する文字列。
この場合、仮にnameが存在しない場合は自動的にURLが文字列として表示されます。

html_optionsにハッシュ形式でclass: "XXX"などと渡すと<a class="XXX">text</a>のように表示してくれます。

また&blockと書いてあることからわかるように、link_toにブロックが渡されていた場合(link_to~do ~endという形式で書かれていた場合)、content_tagにそのままブロックが引き渡されます。


以上で、link_toの読解終わりです。

#大まかな流れ
①代入された引数の順番を整える
②urlやmethodなどの値を整形
③content_tagで表示

以上です。

#今回チャレンジしてみた感想
**一点目**
そもそもメソッドの定義を知ることで、思った通りの出力を出せるようになりそうですね。
例えば、ビューヘルパーであればトライアンドエラーでコードを書く⇔ビルドを繰り返して思った表示を出力していた労力を抑えることができます。

**二点目**
||=や&blockといった書き方については見かけていたものの、なくてもなんとかコードは書けるので意味を理解しておらず良いきっかけになりました。

例えば||=だと、もし変数が未定義またはnilだったら代入、という意味ですが、知らずに書くならばif文を使って複雑な書き方をしてしまうところを、１行でシンプルに表すことができます。

最初に述べた通り、良質でスマートなコードの書き方を学ぶことでメンテナンス性が高く、エラーも起こりづらいコードを書けるようになるのではないでしょうか？

---
今後も継続して学び続けようと思います！