simple_form の f.input が呼ばれた時何が起こっているかコードリーディングして調べてみた

## はじめに

現在、社内の有志で気になる gem をランチを食べながらコードリーディングする、ということを週1回のペースで続けています。

その中で、Rails で form を簡単に実装できる gem である [simple_form](https://github.com/plataformatec/simple_form) において、`f.input` でどんな処理がされているかコードリーディングした記録をまとめようと思います。

## 前提

### 想定読者

Rails で simple_form を使ったことがある

### simple_form のバージョン

現時点で最新の simple_form v4.1.0 を対象に読みます。

https://github.com/plataformatec/simple_form/tree/v4.1.0

### メソッドの記号表記について

コードを読み進めるにあたり、メソッドの記号表記は、[ruby 2.5.0 ドキュメントのヘルプ](https://docs.ruby-lang.org/ja/latest/doc/help.html) を参考にします。

記号|意味|例
:--|:--|:--
`.#`|モジュール関数|`Kernel.#require`
`#`|インスタンスメソッド|`String#size`
`.`|クラスメソッド|`Dir.chdir`

## 読むテーマについて

今回は、以下サンプルコードにおいて、`simple_form_for` メソッド内の `f.input` でどんな処理をしているのかを追っていきます。

```haml
= simple_form_for @user do |f|
  = f.input :username
  = f.input :password
  = f.button :submit
```

## おさらい：simple_form の The Wrappers API について

実際にコードリーディングを始める前に、今回のコードリーディングに関わる前提知識として simple_form の The Wrappers API についておさらいしておきます。

[simple_form の The Wrappers API](https://github.com/plataformatec/simple_form#the-wrappers-api) では、フォームのコンポーネントをどのように render するかを設定、カスタマイズすることができます。

例えば simple_form で以下のような簡単なフォームを作った時を考えます。

```haml
= simple_form_for @user do |f|
  = f.input :username
  = f.input :password
  = f.button :submit
```

上記において、`f.input` や `f.button` といった要素をフォームのコンポーネントと呼びます。

こちらが render されると、若干省いているところもありますが、ざっくり以下のような html が生成されます。

```html
<form novalidate="novalidate" class="simple_form new_user" id="new_user" action="/users/sign_in" accept-charset="UTF-8" method="post">
  <input name="utf8" type="hidden" value="✓">
  <input type="hidden" name="authenticity_token" value="xxx">
  <div class="input email optional user_email">
    <label class="email optional" for="user_email">メールアドレス</label>
    <input class="string email optional" type="email" value="" name="user[email]" id="user_email">
  </div>
  <div class="input password optional user_password">
    <label class="password optional" for="user_password">パスワード</label>
    <input class="password optional" type="password" name="user[password]" id="user_password">
  </div>
  <input type="submit" name="commit" class="btn">
</form>
```

それぞれのコンポーネントにおいて、単純な `input` タグだけではなく、`div` タグで囲われていたり、`label` タグが追加されていたりと、簡単な記述で様々な補完を加えた上で生成してくれているのがわかると思います。

このそれぞれのコンポーネントにおいて、どのような形で html タグを生成するかを指定するのが、simple_form の The Wrappers API です。

The Wrappers APIは、`config.wrappers` を使うことで設定できます。

simple_form をインストールする時に、 `rails generate simple_form:install` と実行すると思いますが、その際 `config/initializers/simple_form.rb` という初期設定ファイルが生成されますよね。

以下に生成されるファイルをコメントを省いて記載します。

```ruby
SimpleForm.setup do |config|
  config.wrappers :default, class: :input,
    hint_class: :field_with_hint, error_class: :field_with_errors do |b|

    b.use :html5
    b.use :placeholder
    b.optional :maxlength
    b.optional :pattern
    b.optional :min_max
    b.optional :readonly

    b.use :label_input
    b.use :hint,  wrap_with: { tag: :span, class: :hint }
    b.use :error, wrap_with: { tag: :span, class: :error }
  end

  config.default_wrapper = :default
  # 以下略
end
```

こちらに記載されている `config.wrappers` ブロック内の設定をいじることで、生成される html をカスタマイズすることができます。

詳細な利用方法は [README](https://github.com/plataformatec/simple_form#the-wrappers-api) に書いてありますので、ご参照ください。

## STEP①： `simple_form_for` メソッドの定義場所を確認する

それではコードを読み進めていきます。

まずサンプルコードで定義した `simple_form_for` メソッドをどこで定義しているかから確認していきます。

* `simple_form_for` メソッドは、`SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for` メソッドから実行される
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/action_view_extensions/form_helper.rb#L14-L29
* 色々 `options` の指定がされているが、最終的に `form_for` メソッドが実行されている
* `form_for` は、[ActionView::Helpers::FormHelper#form_for](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for) メソッドのこと
* `ActionView::Helpers::FormHelper` のメソッドが `SimpleForm::ActionViewExtensions::FormHelper` 内で使えるのは、`ActiveSupport` を使って `include` しているから
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/action_view_extensions/form_helper.rb#L70-L72
* `ActionView::Helpers::FormHelper#form_for` の `builder` オプションとして、 `SimpleForm::FormBuilder` が指定されている

## STEP②： `SimpleForm::FormBuilder` の概要を確認する

ここまでで、`SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for` メソッドで、`ActionView::Helpers::FormHelper#form_for` の `builder` として `SimpleForm::FormBuilder`クラスのオブジェクトが使われていることがわかりました。

次は、`SimpleForm::FormBuilder` の概要をさらっていきます。

* `SimpleForm::FormBuilder` は [ActionView::Helpers::FormBuilder](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html) を継承したもの
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L7
* `f.input` や `f.button` といったメソッドは `SimpleForm::FormBuilder` で定義されている。例えば以下
  * `f.input` : https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L117
  * `f.input_field`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L164
  * `f.association`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L206
  * `f.button`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L236
* ここから、`simple_form_for` のブロック引数 `f` は、`SimpleForm::FormBuilder` クラスのオブジェクトであることがわかる

## STEP③： `SimpleForm::FormBuilder#input` の処理を確認する

ここまでで、`f.input` の処理は、`SimpleForm::FormBuilder#input` メソッドが実行されていることがつかめました。

次は、`SimpleForm::FormBuilder#input` の処理内容を読んでいきます。

* `input` を `find_input`、`wrapper` を `find_wrapper` でそれぞれ検索・指定しつつ、`wrapper.render input` を実行している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L117-L124
* `SimpleForm::FormBuilder#find_input` メソッド では、メソッドの引数 `input_type` に応じて、`SimpleForm::Inputs::TextInput` や `SimpleForm::Inputs::BooleanInput` といったクラスのオブジェクトを戻り値として返している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L529-L539
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L594-L612
* `SimpleForm::FormBuilder#find_wrapper` メソッドでは、`option` の有無や `input_type` に応じて、`render` メソッドに応じるオブジェクトか、`SimpleForm.#wrapper` メソッドの戻り値か、`SimpleForm::FormBuilder#wrapper` メソッドの戻り値かを出し分けしている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L614-L632

## STEP④： `SimpleForm::FormBuilder#wrapper` メソッドの戻り値を確認する

ここまでで、`SimpleForm::FormBuilder#input` メソッドでは、`SimpleForm::Inputs::TextInput` などのオブジェクトを引数に、`wrapper.render` を実行することがわかりましたが、`wrapper` のオブジェクトの実体がまだはっきりしていません。

そのため次は、`SimpleForm::FormBuilder#wrapper` メソッドの戻り値をたどって、`wrapper` の実体を確認します。

* `SimpleForm::FormBuilder#wrapper` メソッドは `attr_reader` を使って定義されている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L8
* `SimpleForm::FormBuilder#wrapper` の実体となるインスタンス変数 `@wrapper` は、 `initialize` 時に値が代入されるが、その値は `SimpleForm.#wrapper` メソッドの実行結果を代入している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/form_builder.rb#L44
* `SimpleForm.#wrapper` メソッドの記述場所は以下。`@@wrappers[name.to_s]` が処理の内容
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L210-L212
* しかし `@@wrappers` の初期値は空の Hash として定義されている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L198
* 実際に `@@wrappers` に値を代入している処理をしているのは、`SimpleForm.#wrappers` メソッド
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L220-L228
* `SimpleForm` モジュール内で `SimpleForm.#wrappers` メソッド実行する処理が書かれていて、ここで `@@wrappers` に代入している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L238-L251
* `SimpleForm.#wrappers` メソッドの内部処理では `SimpleForm.#build` メソッドが実行され、`SimpleForm::Wrappers::Builder` の処理を実行し、その結果を `SimpleForm::Wrappers::Root` クラスのインスタンス変数に含めつつ戻り値として返している
  *  https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L230-L236
* つまり、`SimpleForm::FormBuilder#wrapper` メソッドの戻り値は `SimpleForm::Wrappers::Root` クラスのオブジェクト
  
## STEP⑤： `SimpleForm::Wrappers::Builder` の処理概要を確認する

ここまでで、`wrapper` の実体が `SimpleForm::Wrappers::Root` クラスのオブジェクトであることがわかりました。

しかし、`SimpleForm::Wrappers::Root` のインスタンス変数に含めている`SimpleForm::Wrappers::Builder` の処理は何をしているのでしょうか？少し確認してみます。

* `SimpleForm::Wrappers::Builder` クラスには、`config.wrappers` の中で使われている `b.use` や `b.optional` のようなメソッドが定義されている
  * `b.use`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L49
  * `b.optional`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L57
  * `b.wrapper`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L62
* `SimpleForm::Wrappers::Builder#use` メソッドでは、optionに `:wrap_with` があれば `SimpleForm::Wrappers::Single` クラス、なければ `SimpleForm::Wrappers::Leaf` クラスのオブジェクトを `@components` に配列の要素として代入している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/builder.rb#L49-L55
* `SimpleForm::Wrappers::Root` オブジェクト作成時、インスタンス変数に `builder.to_a` を渡している。つまり、`SimpleForm::Wrappers::Single`や `SimpleForm::Wrappers::Leaf`オブジェクトの配列を渡している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form.rb#L235
* `builder.to_a` を渡された `SimpleForm::Wrappers::Root` のインスタンス変数は、`super(:wrapper, *args)` でクラス継承元の `SimpleForm::Wrappers::Many` クラスのインスタンス変数を一部使っている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/root.rb#L10
* `super(:wrapper, *args)` と記載されているので、`@namespace` に `:wrapper` が入り、その次は `@components`、 それ以降は `@defaults` にまとめて順に代入される
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/many.rb#L15-L21
* つまり `SimpleForm::Wrappers::Root` クラスの `initialize` 時に渡された `builder.to_a` は、インスタンス変数 `@components` となる

## STEP⑥： `wrapper.render input` の処理内容を確認する

ここまでで、`wrapper` の実体が `SimpleForm::Wrappers::Root` クラスのオブジェクトであり、`SimpleForm::Wrappers::Root` クラスのインスタンス変数 `@components` には、`SimpleForm::Wrappers::Single`や `SimpleForm::Wrappers::Leaf`オブジェクトの配列が格納されていることがわかりました。

今まで読んできた内容を踏まえ、一旦`SimpleForm::FormBuilder#input` メソッドに戻り、`wrapper.render input` の処理を追っていきます。

* `wrapper.render input` は `SimpleForm::Wrappers::Root#render` が呼ばれている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/root.rb#L14-L17
* メソッド内部で `super` しているため、`SimpleForm::Wrappers::Root` クラス継承元の `SimpleForm::Wrappers::Many#render` メソッドが呼ばれる
* `SimpleForm::Wrappers::Many#render` メソッド では、空文字列を `html_safe` した `content` に、 `components` ごとに `component.render` の処理結果を文字列に変換したものをどんどん連結させている
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/many.rb#L23-L34
* 最後に `SimpleForm::Wrappers::Many#wrap` メソッドで処理結果を html として整形しつつ返している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/many.rb#L52-L63  

## STEP⑦： `component.render` の処理内容を確認する

ここまでで、`wrapper.render input` が実行されると、整形された html が返ってくるというところまでわかりました。(ようやくhtml変換のところまで来ましたね。。)

あとモヤッとしているところが `component.render` の処理内容だったので、こちらを確認していきます。

* `component.render` の `component` は、`SimpleForm::Wrappers::Single`や `SimpleForm::Wrappers::Leaf` オブジェクト
* `SimpleForm::Wrappers::Single#render` メソッドの処理内容は以下
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/single.rb#L12-L18
* `SimpleForm::Wrappers::Single` のインスタンス変数 `@component` も、実体は `SimpleForm::Wrappers::Leaf` クラスのオブジェクトの模様
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/single.rb#L7
* `SimpleForm::Wrappers::Leaf#render` メソッドを確認すると、メソッド引数 `input` において、`input.method(namespace)` を実行している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/wrappers/leaf.rb#L12-L22 
* `method` メソッドについては ruby の `Object#method` メソッドについてを参照
  * https://docs.ruby-lang.org/ja/latest/method/Object/i/method.html
* `input` の実体は STEP③ で確認したとおり、`SimpleForm::Inputs::TextInput` や `SimpleForm::Inputs::BooleanInput` クラスのオブジェクト
* この `@namespace` に入ってくる値は、`b.use :html5` や `b.use :label_input` でいうと `:html5` や `:label_input` のこと
* `SimpleForm::Inputs::TextInput` や `SimpleForm::Inputs::BooleanInput` クラス継承元の `SimpleForm::Inputs::Base` クラスを確認すると、`SimpleForm::Components::HTML5` や `SimpleForm::Components::LabelInput` といったモジュールを `include` している
  * https://github.com/plataformatec/simple_form/blob/master/lib/simple_form/inputs/base.rb#L20-L29
* include されたそれぞれのモジュールで、`html5` メソッドや `label_input` メソッドを定義してある
  * `SimpleForm::Components::HTML5.#html5`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/components/html5.rb#L9-L18
  * `SimpleForm::Components::LabelInput#label_input`: https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/components/label_input.rb
* `SimpleForm::Components::LabelInput.#label_input` メソッド内で、`label` の有無を判断しつつ、`input` を `namespace` に指定して `method` を `call` している
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/components/label_input.rb#L11-L17
* 上記によって、`SimpleForm::Inputs::TextInput` などのクラスで定義してある `input` メソッドが実行され、内部処理で対象の html が生成される
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/inputs/text_input.rb#L7-L11
* `SimpleForm::Inputs` 以下のクラスは全て `SimpleForm::Inputs::Base` クラスを継承しており、`input` メソッドを実装している。実装していないと `NotImplementedError` となる
  * https://github.com/plataformatec/simple_form/blob/v4.1.0/lib/simple_form/inputs/base.rb#L89-L91
* つまり、`component.render` では `config.wrappers` で定義されたオプションを一つ一つ処理し、html として生成している

## まとめ

ここまで非常に長くなりましたが、`simple_form_for` メソッド内の `f.input` メソッドが実行されるまでの処理をまとめました。

* `SimpleForm::ActionViewExtensions::FormHelper.#simple_form_for` メソッドで、`ActionView::Helpers::FormHelper#form_for` の `builder` として `SimpleForm::FormBuilder`クラスのオブジェクトが使われている
* `f.input` の処理は、`SimpleForm::FormBuilder#input` メソッドが実行されている
* `SimpleForm::FormBuilder#input` メソッドでは、`SimpleForm::Inputs::TextInput` クラスなどのオブジェクトを引数に、`wrapper.render` を実行する
* `wrapper` オブジェクトの実体は `SimpleForm::Wrappers::Root` クラスのオブジェクト
* `SimpleForm::Wrappers::Root` クラスのインスタンス変数 `@components` には、`SimpleForm::Wrappers::Single`や `SimpleForm::Wrappers::Leaf`オブジェクトの配列が格納されている
* `wrapper.render input` が実行されると、整形された html が返ってくる
* `wrapper.render` 内の処理 `component.render` では、 `config.wrappers` で定義されたオプションを一つ一つ処理し、html として生成している

## 終わりに

今回は `f.input` の処理だけをコードで追ってみましたが、それだけでも simple_form のコードを全体的に読むことになり、良い経験になりました。

一気に読み進めたので拙い部分もあると思いますが、もし間違いなどあればコメントいただけるとありがたいです。