gemを読む[3]: Config gemを導入して`rails generate`に思いを馳せる

## 対象gem

今回はConfig gemを読んでいきます。

https://github.com/rubyconfig/config

READMEでは、以下のように説明されています。

> **概要**
> Configは、環境ごとに特定の設定を簡単かつ使いやすい方法で管理するのに役立ちます。
>
> **特徴**
> - シンプルなYAML設定ファイル
> - 設定ファイルはERBをサポート
> - 設定ファイルは継承と複数の環境をサポート
> - 設定情報に便利なオブジェクトメンバーノーテーションでアクセス
> - 複数レベルの設定をサポート (`Settings.group.subgroup.setting`)
> - コードをコミットする際にローカルの開発者設定を無視

## `rails g config:install`は何をするのか

Config gemを使用するには、bundle installしたあとに`rails g config:install`を実行する必要があります。
これにより、configディレクトリ内に設定ファイルｔ複数のymlファイルが作成されます。

### 実装箇所

generateコマンドによって実行される処理は、[lib/generators/config/install_generator.rb](https://github.com/rubyconfig/config/blob/25bc414a188cb05d2558d2e7f6035f6d9430b29c/lib/generators/config/install_generator.rb)で実装されています。

`InstallGenerator`クラスは`::Rails::Generators::Base`クラスを継承しています。
継承元のクラスは[Rails側に実装](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/base.rb)されています。

:::note
`::Rails::Generators::Base`クラスの深堀りは別の機会に取っておきます。
メモ: [ここ](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/actions.rb#L332-L339)にgenerateメソッドが実装されている
:::

さらに`::Rails::Generators::Base`クラスは`Thor::Group`を継承しています。

### 実行される処理

`rails g config:install`コマンドを実行すると、以下の結果を得ることができます。

```bash
% rails g config:install
  create  config/initializers/config.rb
  create  config/settings.yml
  create  config/settings.local.yml
  create  config/settings
  create  config/settings/development.yml
  create  config/settings/production.yml
  create  config/settings/test.yml
  append  .gitignore
```

このコマンドにより、InstallGeneratorオブジェクトを新たに作成し、そのオブジェクトに対して以下のインスタンスメソッドが順に実行されます。

- copy_initializer
- copy_settings
- modify_gitignore

各メソッドの中身を見ていきましょう。

#### copy_initializer

```ruby
def copy_initializer
  template "config.rb", "config/initializers/config.rb"
end
```

`template`メソッドは`::Rails::Generators::Base`の継承元である[Thor gemに実装](https://github.com/rails/thor/blob/main/lib/thor/actions/file_manipulation.rb#L117-L132)されています。

この処理により、[`lib/generators/config/templates/config.rb`](https://github.com/rubyconfig/config/blob/25bc414a188cb05d2558d2e7f6035f6d9430b29c/lib/generators/config/templates/config.rb)がアプリ内の`config/initializers/config.rb`にコピーされます。

#### copy_settings

```ruby
def copy_settings
  template "settings.yml", "config/#{Config.file_name}.yml"
  template "settings.local.yml", "config/#{Config.file_name}.local.yml"
  directory "settings", "config/#{Config.dir_name}"
end
```

`copy_initializer`と同様に、`template`メソッドにより`settings.yml`と`settings.local.yml`がアプリ内の各パスに複製されます。

`directory`メソッドは、`::Rails::Generators::Base`の継承元である[Thor gemに実装](https://github.com/rails/thor/blob/main/lib/thor/actions/directory.rb#L49-L53)されています。
この処理は[`lib/generators/config/templates/settings`](https://github.com/rubyconfig/config/tree/master/lib/generators/config/templates/settings)ディレクトリを、その中に含まれるファイル含めてアプリ内に複製しています。

ここで、`Config.file_name`と`Config.dir_name`は[lib/config.rb](https://github.com/rubyconfig/config/blob/master/lib/config.rb#L13-L33)で初期値が設定されています。
Configの設定値である`Config::Configuration`は[lib/config/configuration.rb](https://github.com/rubyconfig/config/blob/master/lib/config/configuration.rb#L3)に定義されています。

#### modify_gitignore

```ruby
def modify_gitignore
  create_file '.gitignore' unless File.exist? '.gitignore'

  append_to_file '.gitignore' do
    "\n"                                      +
    "config/#{Config.file_name}.local.yml\n"  +
    "config/#{Config.dir_name}/*.local.yml\n" +
    "config/environments/*.local.yml\n"
  end
end
```

ここでは、`.gitignore`にローカル用のsettings.ymlを追加しています。

`append_to_file`メソッドもやはり、[Thor gemに実装](https://github.com/rails/thor/blob/main/lib/thor/actions/file_manipulation.rb#L192-L196)されています。

### 設定ファイルを読んでみる

`copy_initializer`メソッドにより、`config/initializers/config.rb`に[設定ファイル](https://github.com/rubyconfig/config/blob/master/lib/generators/config/templates/config.rb)が作成されます。
このファイルで`Config.file_name`や`Config.dir_name`を設定することができます。

```ruby
Config.setup do |config|
  # Name of the constant exposing loaded settings
  config.const_name = 'Settings'

  # （中略）

  # Name of directory and file to store config keys
  #
  # config.file_name = 'settings'
  # config.dir_name = 'settings'
end
```

Configモジュールに対して`setup`というモジュールメソッドが実行されています。
`setup`は[`lib/config.rb`](https://github.com/rubyconfig/config/blob/master/lib/config.rb#L35-L38)に定義されています。

```ruby
def self.setup
  yield self unless @_ran_once
  @_ran_once = true
end
```

**自己yield**によって、Config自身のインスタンス変数を書き換える処理をおこなっています。
デフォルトでは設定ファイル内の多くの項目がコメントアウトされているので、以下のように値を変更してコメントを外します。

```ruby
  config.file_name = 'filename'
  config.dir_name = 'dirname'
```

これを再び`rails g config:install`してみました。

```bash
           % rails g config:install
    conflict  config/initializers/config.rb
Overwrite /Users/app_path/config/initializers/config.rb? (enter "h" for help) [Ynaqdhm] y
       force  config/initializers/config.rb
      create  config/filename.yml
      create  config/filename.local.yml
      create  config/dirname
      create  config/dirname/development.yml
      create  config/dirname/production.yml
      create  config/dirname/test.yml
      append  .gitignore
```

generatorコマンドにより再度処理が実行されるため、`config/initializers/config.rb`の重複が発生します。
オーバーライドするか聞かれているので、`y`で回答すると、以下の処理がおこなわれました。

- config/initializers/config.rb のオーバーライド
  - templateの値に戻る
  - オーバーライドを`n`にすると、この処理はスキップされる
- `file_name`に応じたymlファイルの作成
- `dir_name`に応じたディレクトリと、その配下へのymlファイルの作成

#### `@_ran_once`について

`set_up`メソッド内で出現する`@_ran_once`は、`yield self`が一度だけ実行されることを保証するインスタンス変数です。
これにより、設定ファイルの意図しない上書きを防いでいます。

これは`set_up`そのものの処理を1度にするだけで、`generate`コマンドは何度も実行することができます。

## まとめ

今回はConfig gemの導入部分についてコードリーディングしてみました。
`rails generate`コマンドで実行されるクラスや、自己yield、`@_ran_once`について知ることができました。

## 参考

https://ysk-pro.hatenablog.com/entry/self-yield