たのしいOSSコードリーディング: Let's read "cookies"🍪

この記事は201９年7月6日に開催された[Tama Ruby会議01](https://tama-rb.github.io/tamarubykaigi01/)での発表「たのしいOSSコードリーディング: Let's read "cookies"🍪」を詳細解説するものです。

Railsアプリケーションで使用される`cookies`メソッドを題材に、このメソッドがどのように実装されているかを読んでいきます。
読みきれなかった部分・知識が曖昧な部分が残っているため、先輩方からの技術的指摘をお待ちしています。

当日の発表資料は[こちら](https://speakerdeck.com/coe401_/tafalsesiiosskodorideingu-lets-read-cookies)

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1857/3fc1dc27-cc82-cc5f-b974-31af162361c7.png)](https://speakerdeck.com/coe401_/tafalsesiiosskodorideingu-lets-read-cookies)


# 調査環境
- Rails 6.0.0.rc1
- TraceLocation 0.9.3.1

# そもそもcookiesメソッドとは
```ruby
cookies[:hoge] = "fuga"
```
Railsが用意しているメソッド。
Cookieに「hoge=fuga」を設定するときに使用。

# 事前知識
## ①Cookie

クライアントからサーバーへリクエストを送る際、HTTPリクエストヘッダはCookieヘッダを含んでいる。

```
クライアント👩‍💻　→　　リクエスト[Cookie: hoge=fuga]　 　→　サーバー👨‍💻
```

Cookieヘッダの中身はこんな感じ。

```
NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ...
```

サーバーからクライアントにレスポンスを送る際、HTTPレスポンスヘッダはSet-Cookieヘッダを含んでいる。

```
クライアント👩‍💻　←　レスポンス[Set-Cookie: hoge=fuga]　←　サーバー👨‍💻
```

Set-Cookieヘッダの中身はこんな感じ。

```
NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; ...
```

いずれの場合も、Cookieは次のように名前と値を一つのペアとしている🍪

```
Cookieの名前=Cookieの値
```
（Rubyスクリプトの中で扱う際、ハッシュの形式`{名前: 値}`に変換したりする）


## ②Rackミドルウェア

### Rackとは
- 「Rack」は次のふたつの意味を持っている
  - WebアプリケーションとWebサーバーを繋ぐプロトコル
  - WebアプリケーションとWebサーバーを繋ぐライブラリ
- Railsアプリケーションは、「Rackプロトコルを満たし、かつRackライブラリを内部で使うRackアプリケーション」

### Rackミドルウェアとは
- Webアプリケーションが持っているべき、「特定の汎用的な機能」を切り出したRackライブラリ
- Railsアプリケーションは多くのRackミドルウェアを使用している
- 今回見ていく`ActionDispatch::Cookies`はRailsで使用されているRackミドルウェアのひとつ

```ruby
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
　(略)...
```

### `ActionDispatch::Cookies`とは？
- Railsアプリケーションで使用されているRackミドルウェアのひとつ
- Cookieを保存するために使う

```ruby
$ rails middleware
　(略)...
use ActionDispatch::Cookies # ←これ
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes
```

それでは早速読んでいきましょう🍪

# `cookies[:hoge] = "fuga"`の全体図
[trace_location](https://github.com/yhirano55/trace_location)を使用すると、cookiesメソッドを呼んだ際、次のように一連の処理が走ることが確認できます。

```ruby
Logged by TraceLocation gem at 2019-06-05 20:31:31 +0900
https://github.com/yhirano55/trace_location

[Tracing events] C: Call, R: Return

C actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12 [ActionController::Cookies#cookies]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11 [ActionDispatch::Request#cookie_jar]
    C rack-2.0.7/lib/rack/request.rb:58 [Rack::Request::Env#fetch_header]
    R rack-2.0.7/lib/rack/request.rb:60 [Rack::Request::Env#fetch_header]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:15 [ActionDispatch::Request#cookie_jar]
R actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:14 [ActionController::Cookies#cookies]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374 [ActionDispatch::Cookies::CookieJar#[]=]
  C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350 [ActionDispatch::Cookies::CookieJar#handle_options]
  R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:370 [ActionDispatch::Cookies::CookieJar#handle_options]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:392 [ActionDispatch::Cookies::CookieJar#[]=]

Result: fuga
```

要約するとこんな感じ

```ruby
[Rails]ActionDispatch::Cookiesミドルウェアにアクセスする

[Rails][Rack]リクエストから既存のCookieヘッダを見つけて返す
             見つからなかった場合は空のCookieをセットする

[Rails]ActionDispatch::Cookiesのインスタンスを生成
　　  　インスタンス変数を初期化する

[Rails]ActionDispatch::Cookiesのインスタンス変数に
　　  　新しいCookieの値を追加する

[Rails]ミドルウェアがアプリケーションを返すタイミングで
　  　　インスタンス変数の内容をSet-Cookieヘッダに書き込む(?) ← ここは処理を追いきれず…
```

RailsとRackの処理を行ったり来たりするため、手元にソースコードがある方は確認しながら進むことをお勧めします。

# ひとつずつ読んでいきましょう🍪

## rails/actionpack/lib/action_controller/metal/cookies.rb  

```ruby
      7 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12#cookies
```

Rails側から処理を追います🍪
まずは`ActionController::Cookies#cookies`メソッドを読んでみましょう。

### ActionController::Cookies#cookies

```actionpack/lib/action_controller/metal/cookies.rb
 11     private
 12       def cookies
 13         request.cookie_jar
 14       end
 ```

一番最初に呼ばれる`#cookies`メソッドの中身はこんな感じ、レシーバに対して`#cookie_jar`メソッドを呼んでいるだけ。

レシーバの`request`は`ActionDispatch::Request`のインスタンス（※include先で定義されている）。

続いて`#cookie_jar`メソッドを読んでみましょう。

## rails/actionpack/lib/action_dispatch/middleware/cookies.rb

```ruby
      8   C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11#cookie_jar
```

引き続きRails🍪

### ActionDispatch::Request#cookie_jar

```actionpack/lib/action_dispatch/middleware/cookies.rb
 11     def cookie_jar
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end
 15     end
 ```

`#cookie_jar`メソッドの中身はこんな感じ。
一行目でRackのメソッドである`fetch_header`(`Rack::Request::Env#fetch_header`)を呼んでいます。

続いて`Rack::Request::Env#fetch_header`を読んでみましょう。

## rack/lib/rack/request.rb

```ruby
      9     C /vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/request.rb:58#fetch_header
```

Rack側に移ります🍪

### Rack::Request::Env#fetch_header

```rack/lib/rack/request.rb
     69       def fetch_header(name, &block)
     70         @env.fetch(name, &block)
     71       end
```

`#fetch_header`メソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

#### nameとは

引数の`name`は`"action_dispatch.cookies"`

（参考）

```actionpack/lib/action_dispatch/middleware/cookies.rb 
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 14       end
```

#### &blockとは

引数の`&block`は`fetch_header`に渡されているブロック
`self.cookie_jar = Cookies::CookieJar.build(self, cookies) `。

（参考）

```actionpack/lib/action_dispatch/middleware/cookies.rb 
 12       fetch_header("action_dispatch.cookies") do
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies) 
 14       end
```

#### レシーバ`@env`とは

HTTPヘッダを表すハッシュ。
例えばこんなRackアプリがあった場合

```ruby
['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
```

真ん中の`{'Content-Type' => 'text/html'}`というハッシュが、Rack上では`@env`というインスタンス変数に入っています。

### つまり…
`fetch_header`は、ヘッダである`@env`ハッシュに対して、[Hash#fetch](https://docs.ruby-lang.org/ja/2.6.0/method/Hash/i/fetch.html)メソッドを呼ぶための処理。

すなわち

- `@env`が `"action_dispatch.cookies"`というkeyを持っていた場合：
  - ➡︎そのvalueが返る
- `@env`が `"action_dispatch.cookies"`というkeyを持っていなかった場合；
  - ➡︎ブロック`self.cookie_jar = Cookies::CookieJar.build(self, cookies)`が実行される

続いて、ブロック`self.cookie_jar = Cookies::CookieJar.build(self, cookies)`が実行された場合の処理を追います。

## rails/actionpack/lib/action_dispatch/middleware/cookies.rb

`self.cookie_jar =`は、`ActionDispatch::Request#cookie_jar=`としてメソッド定義されています。
早速読んでみましょう🍪

### ActionDispatch::Request#cookie_jar=

```actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end
```

ここで登場する引数の`jar`の正体は`Cookies::CookieJar.build(self, cookies)`ですが、この処理は後で読むので一旦パス。

二行目で、再びRackのメソッドである`set_header`を呼んでいます。

`#set_header`の処理を読んでみましょう。

## rack/lib/rack/request.rb

再びRackへ🍪

### Rack::Request::Env#set_header

```lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end
```

`#fetch_header`メソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。

#### 引数nameとは

引数の`name`は"action_dispatch.cookies"

#### 引数vとは
`v`は先程の`jar`(つまり`Cookies::CookieJar.build(self, cookies)`)

具体的には

```ruby
@env["action_dispatch.cookies"] = Cookies::CookieJar.build(self, cookies)
```

という処理が走っています。
このとき、返り値は`Cookies::CookieJar.build(self, cookies)`になります。

## ここまでのまとめ
アプリケーションで`cookies`メソッドが呼ばれると、次の2つの処理が走る。
1. `@env`ハッシュにおける`"action_dispatch.cookies"`keyの存在を確認する。
2. 存在する場合はvalueを返す。
     存在しない場合は新たに`{"action_dispatch.cookies":
     *Cookies::CookieJar.build(self, cookies)*}`というペアをつくる。

# Let's read Rack::Request::Helpers#cookies🍪

ここからは、先ほどパスした`Cookies::CookieJar.build(self, cookies)`を読んでいきます。

## ActionDispatch::Cookies::CookieJar.build

```actionpack/lib/action_dispatch/middleware/cookies.rb
     11     def cookie_jar
     12       fetch_header("action_dispatch.cookies") do
     13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
     14       end
     15     end
```

↑13行目で`self.cookie_jar = `に渡している`Cookies::CookieJar.build(self, cookies)`

### が、その前に

注目:eyes:

```ruby
Cookies::CookieJar.build(self, cookies)
```

### ここで引数に入れたものは何？

```ruby
(self, cookies)
```

`self`は一番最初に出てきた`request`
`cookies`は、これもRackのメソッドを呼んでいます。

## rack/lib/rack/request.rb

cookiesの正体を探るため、`Rack::Request::Helpers#cookies`メソッドを読んでいきましょう🍪

## Rack::Request::Helpers#cookies
```lib/rack/request.rb
    215       def cookies
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end
    219         string = get_header HTTP_COOKIE
    220 
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
    222         hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end
```

`#cookies`の中身はこんな感じ。

## 一行づつ

```lib/rack/request.rb
    216         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
    217           set_header(k, {})
    218         end
```

`fatch_header`は先程と同じメソッドで、`@env`ハッシュに対して`RACK_REQUEST_COOKIE_HASH`keyを`fetch`します。

- keyがあった場合
  - ➡️valueを返す
- keyがなかった場合
  - ➡️これも先程登場した`set_header`メソッドを使用して、
`@env`ハッシュに`RACK_REQUEST_COOKIE_HASH: {}`というペアを追加する
  - 返り値は空の`{}`

（参考）

```lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end
```

いずれの場合も、返り値を`hash`に代入しています。

#### RACK_REQUEST_COOKIE_HASHとは？
ここで登場する、`RACK_REQUEST_COOKIE_HASH`の正体は、`rack.request.cookie_hash`。
Rackがサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

```example.rb
{"_todo_session"=>"BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg==--c34c5fc6de928cde391cccd2b710547c7aab1d06"}
```
（↑の例はたまたま下記の記事中で見つけたものです）
[Rails で捕捉されない例外が発生したらメールを送る](https://qiita.com/labocho/items/adb89ede814b86e9c04a)

### つづきまして

```lib/rack/request.rb  
    219         string = get_header HTTP_COOKIE
```

`HTTP_COOKIE`を引数として、`get_header`メソッドを呼んでいます。
`get_header`は`Rack::Request::Env#get_header`で、定義は以下の通り。

```lib/rack/request.rb  
    63          def get_header(name)
    64            @env[name]
    65          end
```

`@env`ハッシュから`HTTP_COOKIE`keyを探し、valueを返し、返り値が`string`に代入されます。
ただし、初めてCookieを使用する場合は`@env`ハッシュに`HTTP_COOKIE`keyが見つからないため、`nil`が返ります。

#### HTTP_COOKIEとは？

ここで登場する`HTTP_COOKIE`の正体は、リクエストメッセージに含まれるメタ変数で、`Cookie`ヘッダを返しています。
中身はこんな感じ（Cookieの名前と値を=で結ぶことでペアとして表現している）
(例)

```example.rb _todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06
```

（↑の例も先ほどの記事よりお借りしました）

※メタ変数について：
[参照](https://wiki.suikawiki.org/n/%E3%83%A1%E3%82%BF%E5%A4%89%E6%95%B0#header-section-%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB%E4%BE%9D%E5%AD%98%E3%81%AE%E3%83%A1%E3%82%BF%E5%A4%89%E6%95%B0)
> 例えば HTTP というプロトコルにおいては、 HTTP_USER_AGENT という名前のメタ変数が HTTP 要求メッセージの User-Agent: 欄の値を提供することになっています

### つづき

初めてCookieを使用する場合、ここまでで

- `hash`に`{}`
- `string`に`get_header HTTP_COOKIE`の返り値（初めてCookieを使用する場合は`nil`）

が代入されていることになります。

```lib/rack/request.rb  
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
```

`RACK_REQUEST_COOKIE_STRING`を引数に、`Rack::Request::Env#get_header`メソッドを呼んで`string`と比較。

（参考）

```lib/rack/request.rb  
    63          def get_header(name)
    64            @env[name]
    65          end
```


#### RACK_REQUEST_COOKIE_STRINGとは？

ここで登場する`RACK_REQUEST_COOKIE_STRING`の正体は`rack.request.cookie_string`で、こちらもサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)

```example.rb
_todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06
```
（御多分に洩れず↑の例も先ほどの記事より）

…`HTTP_COOKIE`と同じですね！

### つづき

```lib/rack/request.rb  
    221         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
```

`==`の場合はここで`return hash`

#### なぜ？
次行以降はリクエストヘッダからやって来た`hash`(初めてのCookieを使用する場合は空の`{}`)を`HTTP_COOKIE`で更新する処理となっています。
ハッシュの中身が変更されていない場合、置き換える必要がないため。

初めてCookieを使用する場合、`string`と `get_header(RACK_REQUEST_COOKIE_STRING)`の返り値はいずれもnilのため、ここで`cookies`メソッドの処理は`hash`(空の`{}`)を返して終了します。

### つづき（すでにCookieが存在する場合のみ）

```lib/rack/request.rb  
    222         hash.replace Utils.parse_cookies_header string
```

ここでは、
`Utils.parse_cookies_header(string)`の返り値`{ Cookieの名前: Cookieの値 }`で、`hash`(`fetch_header(RACK_REQUEST_COOKIE_HASH)`の返り値)を`replace`しています。

[Hash#replace](https://docs.ruby-lang.org/ja/2.6.0/method/Hash/i/replace.html)

`Utils.parse_cookies_header`は`Rack::Utils`のメソッドで、
詳しくは読みませんが、
`string`(`Cookieの名前=Cookieの値`)を`{Cookieの名前: Cookieの値}`
のようなハッシュに変換する役割を担っています。

（参考）

```lib/rack/utils.rb
    209     def parse_cookies_header(header)
    215       cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
    216       cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
    217     end
```

（↑`each_with_object`を使用して`{}`に名前と値を代入している）

### つづき（すでにCookieが存在する場合のみ）

```lib/rack/request.rb  
    223         set_header(RACK_REQUEST_COOKIE_STRING, string)
    224         hash
    225       end
```

すなわち、ここで

```ruby
@env[RACK_REQUEST_COOKIE_STRING] = string
```
という処理を実行。

（参考）

```lib/rack/request.rb
     79       def set_header(name, v)
     80         @env[name] = v
     81       end
```

最後に`hash`(`{Cookieの名前: Cookieの値}`) を返しています。

## ここまでのまとめ

メソッド`cookies`を呼ぶとき

- 初めてCookieを使用する場合: ヘッダ(`@env`)に`{RACK_REQUEST_COOKIE_HASH: {} }`が追加される
  - 最終的な返り値は空の`{}`
- すでにCookieが存在する場合： ヘッダ(`@env`)に`{RACK_REQUEST_COOKIE_STRING: リクエストメッセージのCookieヘッダ}`が追加される
  - 最終的な返り値は`{Cookieの名前: Cookieの値}`

# 改めましてActionDispatch::Cookies::CookieJar.build
## actionpack/lib/action_dispatch/middleware/cookies.rb

お待たせしました。`Cookies::CookieJar.build`を読んでいきましょう。
再びRailsへ🍪

### ActionDispatch::Cookies::CookieJar.build

```actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar|
    291           jar.update(cookies)
    292         end
    293       end
```

`Cookies::CookieJar.build`の中身はこんな感じ。

`tap`を使用して処理をチェーンしているのは、最終的にできあがったCookies::CookieJarインスタンスを返すためではないかと考えられます。
（`tap`を挟まない場合、返り値が`jar.update`の実行結果になる）

ここで登場するオブジェクトを確認しましょう。

#### reqとは
引数の`req`は一番最初の`request`（`ActionDispatch::Request`のインスタンス）

#### cookiesとは
`cookies`は先ほど読んだ`Rack::Request::Helpers#cookies`の返り値(`{Cookieの名前: Cookieの値}`あるいは空の`{}`)

### Cookies::CookieJar.buildは何をやっているのか

ここでの役割は２つ

- `Cookies::CoookieJar`のインスタンスを作る
- できたインスタンスに対して`Cookies::CookieJar#update`メソッドを呼ぶ

順番に処理を読んでいきます。

### new(req)

```actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    289       def self.build(req, cookies)
    290         new(req).tap do |jar|　# ←
    291           jar.update(cookies)
    292         end
    293       end
```

`new(req)`(インスタンス化)すると、`initialize`が呼ばれます。

```actionpack/lib/action_dispatch/middleware/cookies.rb
    295       attr_reader :request
    296 
    297       def initialize(request)
    298         @set_cookies = {}
    299         @delete_cookies = {}
    300         @request = request
    301         @cookies = {}
    302         @committed = false
    303       end
```

`ActionDispatch::Cookies::CookieJar#initialize`は各インスタンス変数を初期化するだけの処理。

`@request`には引数で渡されている一番最初の`request`が入ります。
`attr_reader`によってメソッドとしてアクセスできるようになります。

### つづいてtap以下の処理

```actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    290         new(req).tap do |jar|
    291           jar.update(cookies)　# ←
    292         end
```

`tap`ブロックの中で実行されている`update`メソッドはこちら

```actionpack/lib/action_dispatch/middleware/cookies.rb
    334       def update(other_hash)
    335         @cookies.update other_hash.stringify_keys
    336         self
    337       end
```

### 引数other_hashとは

引数`other_hash`は先ほどの`cookies`、つまり`{Cookieの名前: Cookieの値}`もしくは空の`{}`。

### `@cookies`とは
`@cookies`は`initialize`の中で初期化したこの部分

```ruby
    301         @cookies = {}
```

ここでは、この空の`{}`に対して`update`メソッドを呼んでいます。
`Hash#update`メソッドは`Hash#merge!`のエイリアス。

処理を実行すると、こうなります。

```ruby
@cookies = {'Cookieの名前': 'Cookieの値'}
```

(`other_hash`が空の`{}`の場合は、何も起きない)

その後、`self` (=`jar`、つまり`Cookies::CookieJar.build`でできたインスタンス)を返しています。

(余談)
↑の方で`tap`を使った理由を「インスタンスを返すため」と記述しているのですが、ここでもインスタンス自身を返しているため、どちらか片方の処理で良いのではという疑惑あり…PRチャンス？

また、先述の通りこのインスタンスは

```actionpack/lib/action_dispatch/middleware/cookies.rb
 13         self.cookie_jar = Cookies::CookieJar.build(self, cookies)
 ```

ここで`self.cookie_jar=`メソッドに引数として渡されて、

```actionpack/lib/action_dispatch/middleware/cookies.rb
     28     def cookie_jar=(jar)
     29       set_header "action_dispatch.cookies", jar
     30     end
```

ここで`@env = {"action_dispatch.cookies": できあがったインスタンス}`としてヘッダーに追加されます。

## ここまでのまとめ

アプリケーションから`cookies`メソッドを呼ぶと、結果的には次のようなリクエストヘッダができる

```
@env = {
  "action_dispatch.cookies": Cookies::CookieJarのインスタンス},
  # 初めてCookieを使用する場合
  RACK_REQUEST_COOKIE_HASH: {},
  # すでにCookieが存在する場合
  RACK_REQUEST_COOKIE_STRING: リクエストメッセージから受け取ったCookieヘッダ
}
```

# 最後に[]=

```ruby
cookies[:hoge] = :fuga
```

`cookies`に値を代入する処理を見ていきます🍪

## actionpack/lib/action_dispatch/middleware/cookies.rb 

```ruby
14 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374#[]=
```

ここからはRailsのお話🍪
`#[]=`メソッドを読んでいきましょう。

### ActionDispatch::Cookies::CookieJar#[]=

```actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    272 #   class CookieJar
    # ...
    372       # Sets the cookie named +name+. The second argument may be the cookie's
    373       # value or a hash of options as documented above.
    374       def []=(name, options)
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else
    379           value = options
    380           options = { value: value }
    381         end
    382 
    383         handle_options(options)
    384 
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end
```

`ActionDispatch::Cookies::CookieJar#[]=`の中身はこんな感じ。

## 一行ずつ

```actionpack/lib/action_dispatch/middleware/cookies.rb
    374       def []=(name, options)
```

これを定義すると、

```ruby
[:name] = options
```

という形で値を代入できるようになります。

### それではここで、Railsアプリからcookies[]= に渡せる値を確認しましょう🍪

[Railsガイド](https://edgeapi.rubyonrails.org/classes/ActionDispatch/Cookies.html)

`cookie[:hoge] = "fuga"`のように文字列を渡すことが多いと思いますが、オプションを細かく設定する場合、右辺にはハッシュを渡すこともできます。

```ruby
cookies[:hoge] = {
  value:      'Cookieの値',
  expires:    'Cookieの有効期限',
  path:       'Cookieが適用されるパス(デフォルトは/)',
  domain:     'Cookieが適用されるドメイン',
  tld_length: 'domain: :allの時、TLDの一部として解釈される短い（3文字以下の）ドメインを使用するときに、TLDの長さを明示的に設定',
  secure:     '暗号化通信のみを有効化(デフォルトはfalse)',
  httponly:   'スクリプト経由もしくはHTTP通信のみを有効化(デフォルトはfalse)'
}
```

これを踏まえて、引き続き処理を読んでいきます。

##  ①引数optionsがハッシュの場合

```actionpack/lib/action_dispatch/middleware/cookies.rb
    375         if options.is_a?(Hash)
    376           options.symbolize_keys!
    377           value = options[:value]
    378         else
```

`cookies[:hoge] = {"value" => "fuga}`の場合、`symbolize_keys!`で`{:value => "fuga"}`に変換。

変数`value`に`"fuga"`を代入。

##  ②引数optionsがハッシュ以外の場合

```actionpack/lib/action_dispatch/middleware/cookies.rb
    378         else
    379           value = options
    380           options = { value: value }
    381         end
```

`cookies[:hoge] = "fuga"`の場合、変数`value`に`"fuga"`を代入。

`options`に `{value: "fuga"}`を再代入。

## 今の状態

①②いずれの場合も、

- `options`は`{value: "fuga"}`
- `value`は`fuga`

となっている。

### つづき

```actionpack/lib/action_dispatch/middleware/cookies.rb
    383         handle_options(options)
```

同じクラスの`handle_options(options)`を呼びます。

### ActionDispatch::Cookies::CookieJar#handle_options

`handle_options`は`options`をいい感じにするためのメソッドです。

```actionpack/lib/action_dispatch/middleware/cookies.rb
    350       def handle_options(options) # :nodoc:
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end
    354 
    355         options[:path] ||= "/"
    356 
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end
```

中身はこんな感じ。

### 一行ずつ

```actionpack/lib/action_dispatch/middleware/cookies.rb
    351         if options[:expires].respond_to?(:from_now)
    352           options[:expires] = options[:expires].from_now
    353         end
```

`cookies`メソッドに有効期限を設定した場合(例：`1.week`)、`from_now`で具体的な日時に変換して`options[:expires]`に再代入。
`from_now`は`since`のエイリアス。

```actionpack/lib/action_dispatch/middleware/cookies.rb
    355         options[:path] ||= "/"
```

`cookies`メソッドにパスが指定されて`いない`場合、ルートパスを設定（デフォルト値として）

```actionpack/lib/action_dispatch/middleware/cookies.rb
    357         if options[:domain] == :all || options[:domain] == "all"
    358           # If there is a provided tld length then we use it otherwise default domain regexp.
    359           domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
    360 
    361           # If host is not ip and matches domain regexp.
    362           # (ip confirms to domain regexp so we explicitly check for ip)
    363           options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
    364             ".#{$&}"
    365           end
    366         elsif options[:domain].is_a? Array
    367           # If host matches one of the supplied domains without a dot in front of it.
    368           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
    369         end
    370       end
```

誰か正規表現に強い方…………

- `[:domain]`オプションが`:all`or`"all"`の場合
  - [:tld_length]が指定されている場合は`/([^.]+\.?){#{options[:tld_length]}}$/`
  - そうでない場合は`DOMAIN_REGEXP`
 
  を変数`domain_regexp`に代入
(※`DOMAIN_REGEXP`は`/[^.]*\.([^.]*|..\...|...\...)$/`)

  リクエストメッセージのホストが`/^[\d.]+$/`に一致しない場合&&変数`domain_regexp`に一致する場合は`options[:domain]`を`".#{$&}"`で置き換える

- `[:domain]`オプションが配列の場合
  - `options[:domain]`を、リクエストメッセージのホストが含まれる要素で置き換える

## ここでやったこと

- 有効期限を具体的な日時に変換
- パスの設定がない場合にデフォルト値を設定
- ドメインをいい感じに再代入(？)

## ActionDispatch::Cookies::CookieJar#[]=のつづき

### 今の状態

①②いずれの場合も、
- `options`は`{value: 'fuga'}`
- `value`は`fuga`

となっています。

### ActionDispatch::Cookies::CookieJar#[]=のつづき

```actionpack/lib/action_dispatch/middleware/cookies.rb
    385         if @cookies[name.to_s] != value || options[:expires]
    386           @cookies[name.to_s] = value
    387           @set_cookies[name.to_s] = options
    388           @delete_cookies.delete(name.to_s)
    389         end
    390 
    391         value
    392       end
```

`@cookies["hoge"]`のvalueが`"fuga"`でない場合(つまり値が変わっている場合)、もしくは有効期限の設定がない場合に、

- `@cookies["hoge"]`に`"fuga"`を代入
- `@set_cookies`にoption`{value: 'fuga'}`を代入
- `@delete_cookies`から`[:hoge]`を削除する

をして、最後に`"fuga"`を返します。

# 最後に
trace_locationでログに残っていたのはここまでですが、
先述の通り`ActionDispatch::Cookies`は`call`メソッドを持ったRackミドルウェアとして実装されており、
Railsアプリケーションから使用されています。

```ruby
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies # ←ここ 
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes
```

このことから、Railsアプリケーションにリクエストがあった際、Rackアプリとして次の処理が呼ばれることがわかります。

```actionpack/lib/action_dispatch/middleware/cookies.rb
    170 # class Cookies
    # ...
    637     def initialize(app)
    638       @app = app
    639     end
    640 
    641     def call(env)
    642       request = ActionDispatch::Request.new env
    643 
    644       status, headers, body = @app.call(env)
    645 
    646       if request.have_cookie_jar?
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end
    654       end
    655 
    656       [status, headers, body]
    657     end
```

中身は詳しく追っていきませんが、

```actionpack/lib/action_dispatch/middleware/cookies.rb
    647         cookie_jar = request.cookie_jar
    648         unless cookie_jar.committed?
    649           cookie_jar.write(headers)
    650           if headers[HTTP_HEADER].respond_to?(:join)
    651             headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
    652           end
    653         end
```

この辺りでヘッダーへの書き込みが行われていることが確認できました。

# 今回追うことができなかった処理・残された疑問点
- `"rack.request.cookie_hash"`がヘッダーに書き込まれるタイミング
- `Cookies::CookieJar#write`→`Cookies::CookieJar#make_set_cookie_header`から、最終的に呼ばれる`::Rack::Utils.add_cookie_to_header`（↑の資料では未出）
  - ヘッダーに書き込む文字列の成形を行なっていることが確認できるが、その後実際にヘッダーに書き込まれるタイミング

# おつかれさまでした🍪

## 参考資料
- [Ruby on Railsチュートリアル 第9章 発展的なログイン機構 9.1.2 ログイン状態の保持](https://railstutorial.jp/chapters/advanced_login?version=5.1#sec-login_with_remembering)
- [RFC 6265 HTTP 状態管理の仕組み（ “HTTP cookie” ）](https://triple-underscore.github.io/http-cookie-ja.html)
- [SuikaWiki メタ変数 - プロトコル依存のメタ変数](https://wiki.suikawiki.org/n/%E3%83%A1%E3%82%BF%E5%A4%89%E6%95%B0#header-section-%E3%83%97%E3%83%AD%E3%83%88%E3%82%B3%E3%83%AB%E4%BE%9D%E5%AD%98%E3%81%AE%E3%83%A1%E3%82%BF%E5%A4%89%E6%95%B0)
- [Rack: a Ruby Webserver Interface](https://rack.github.io/)
- [TraceLocation](https://github.com/yhirano55/trace_location)

## おまけ

TraceLocationで作成したログは次の通りです（マークダウン形式）
Generated by [trace_location](https://github.com/yhirano55/trace_location) at 2019-06-05 20:28:50 +0900

<details open>
<summary>actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12</summary>

##### ActionController::Cookies#cookies

```ruby
def cookies
  request.cookie_jar
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8
```
</details>
<details open>
<summary>actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11</summary>

##### ActionDispatch::Request#cookie_jar

```ruby
def cookie_jar
  fetch_header("action_dispatch.cookies") do
    self.cookie_jar = Cookies::CookieJar.build(self, cookies)
  end
end
# called from actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:13
```
</details>
<details open>
<summary>rack-2.0.7/lib/rack/request.rb:58</summary>

##### Rack::Request::Env#fetch_header

```ruby
def fetch_header(name, &block)
  @env.fetch(name, &block)
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:12
```
</details>
<details open>
<summary>actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374</summary>

##### ActionDispatch::Cookies::CookieJar#[]=

```ruby
def []=(name, options)
  if options.is_a?(Hash)
    options.symbolize_keys!
    value = options[:value]
  else
    value = options
    options = { value: value }
  end

  handle_options(options)

  if @cookies[name.to_s] != value || options[:expires]
    @cookies[name.to_s] = value
    @set_cookies[name.to_s] = options
    @delete_cookies.delete(name.to_s)
  end

  value
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8
```
</details>
<details open>
<summary>actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350</summary>

##### ActionDispatch::Cookies::CookieJar#handle_options

```ruby
def handle_options(options) # :nodoc:
  if options[:expires].respond_to?(:from_now)
    options[:expires] = options[:expires].from_now
  end

  options[:path] ||= "/"

  if options[:domain] == :all || options[:domain] == "all"
    # If there is a provided tld length then we use it otherwise default domain regexp.
    domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP

    # If host is not ip and matches domain regexp.
    # (ip confirms to domain regexp so we explicitly check for ip)
    options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
      ".#{$&}"
    end
  elsif options[:domain].is_a? Array
    # If host matches one of the supplied domains without a dot in front of it.
    options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
  end
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:383
```
</details>