Railsの自動読み込みにオリジナルの変換処理を追加する

この記事は [CAMPFIRE Advent Calendar 2022](https://qiita.com/advent-calendar/2022/campfire) 8日目の記事です。

アドベントカレンダー参加という良い機会に、ふと気になったRailsの自動読み込み(autoload)について調べてみました。

# 前提
以下の環境で検証しています
- OS: macOS
- Rubyバージョン: 2.7.6
- Railsバージョン: 7.0.4
- Railsオートローダ: Zeitwerk

また、[Railsガイド](https://railsguides.jp/autoloading_and_reloading_constants.html) の表現に基づき`autoload`を「自動読み込み」として称しています


# Railsの自動読み込み(オートロード)
Railsで利用されているオートローダはRails6以降のバージョンで変更され現在は[Zeitwerk](https://github.com/fxn/zeitwerk)というgemに任されています

Rails5以前のオートローダは「classic」と称され、Rails6.x の場合のみ、classicとzeitwerkモードの切り替えが可能です
参考: [4 zeitwerkモードを有効にする](https://railsguides.jp/classic_to_zeitwerk_howto.html#zeitwerk%E3%83%A2%E3%83%BC%E3%83%89%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B)

|バージョン|オートローダ|モードの変更|
|----|----|----|
|Rails 5以前| Active Support で実装 (`classic`) |classicのみ|
|Rails 6.x| Zeitwerk (`zeitwerkモード`)| classicに変更可能 |
|Rails 7| Zeitwerk (`zeitwerkモード`)|zeitwerkモードのみ|


## classic と zeitwerk モード の違い

2つの違いについて詳細は以下の記事で詳しく記載されています
参考:[Zeitwerkの壊し方](https://qiita.com/fursich/items/717a720d9f4465e4cbbb)

大きな違いはファイル名とクラス(やモジュール)の紐付け方法で以下のような違いがあります

- classic: `定数名から`ファイルを探す
- Zeitwerk: `ファイル名`から定数を推測する(ファイル名をキャメルケースに変換する)

このため、classicから Zeitwerkに以降すると 新ルールに則っていないクラスは場合にcamelizeに失敗します。
 例) `html_parser.rb`は`HTMLParser`で認識できず`HtmlParser`でないと動かなくなる

## Zeitwerk
これからのRailsで使われていくZeitwerk
これを知れば、Railsの自動読み込みを理解できる…！
ということでZeitwerkでできることを紹介しようと思っていたのですが
ちょうど4日前にとてもわかりやすい日本語記事が公開されていました。
[Zeitwerk を使いこなしたい](https://zenn.dev/megeton/articles/a3eac8f77fe1cd)

とても丁寧にかかれていて、 上記の記事と[ZeitwerkのReadme](https://github.com/fxn/zeitwerk) を合わせて読めばかなり理解が進むと思います！

# Railsでのカスタマイズ

自動読み込みの対象となる ディレクトリを追加したい場合は configで設定が可能です
参考: [Railsガイド 定数の自動読み込みと再読み込み (Zeitwerk)](https://railsguides.jp/autoloading_and_reloading_constants.html#config-autoload-paths)

## 詳細な設定
また、`config/initializers/`にファイルを追加し、
Zeitwerk の設定を記載することもできます
参考: [定数の自動読み込みと再読み込み (Zeitwerk) 10 活用形をカスタマイズする](https://railsguides.jp/autoloading_and_reloading_constants.html#%E6%B4%BB%E7%94%A8%E5%BD%A2%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B)
### 例1) 特定のディレクトリを除外する
```ruby
# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector = Zeitwerk::Inflector.new

  # app/settings を除外
  autoloader.ignore "app/settings"
end
```
### 例2) html_parser.rb のクラスを HTMLParser として利用する(通常はHtmlParser)
```ruby
# config/initializers/zeitwerk.rb

Rails.autoloaders.each do |autoloader|
  autoloader.inflector = Zeitwerk::Inflector.new
  
  #案1) 
  # autoloader.inflect.acronym "HTML"
  # 案2)
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
  )
end
```

# Railsの自動読み込みにオリジナルの変換処理を追加する
本題です。
更に自由な変更を加えたい場合、インフレクタ(inflector)をオリジナルのインフレクタに変更することでより自由に修正を入れることができます

 ※基本的にはデフォルトのインフレクタの利用したほうが後々混乱も誘発せず望ましいと思います。

## 手順

  `MyInflector`を作成して上記の変換の処理を実装する場合

### 1. オリジナルのインフレクタ作成
`Zeitwerk::Inflector` を継承してオリジナルのインフレクタを作成します
```ruby
#lib/inflector/my_inflector.rb

class MyInflector < Zeitwerk::Inflector
  def camelize(basename, abspath)
    if basename =~ /\Ahtml_(.*)/
      "HTML" + super($1, abspath)
    else
      super
    end
  end
end
```

### 2. 作成したインフレクタを読み込み、オートローダに設定

`autoloader.inflector = MyInflector.new`
で自動読み込みで利用するインフレクタを設定します
```ruby
# config/initializers/zeitwerk.rb

require 'inflector/my_inflector'
Rails.autoloaders.each do |autoloader|
autoloader.inflector = MyInflector.new
end
```

 
## 解説: def camelize について
### パラメータ
MyInflectorの`camelize`メソッドには以下のパラメータが渡され、これらを元にクラス名やモジュール名の変換が可能になります
- basename: 拡張子を含まないファイル名
- abspath:  ファイルやディレクトリの絶対パス
  ```
  例) {PATH}/app/controllers/application_controller.rb

  basename: application_controller
  abspath: {PATH}/app/controllers/application_controller.rb
  ```
### 変換の処理

```ruby
if basename =~ /\Ahtml_(.*)/
      "HTML" + super($1, abspath)
```

「html_」から始まるファイルの「html_」をHTMLにし、残りはそのままキャメルケースに変換している


例えば「HTML」を「YML」のように変更した場合は「html_perser.rb」というファイルのクラスを「YMLParser」として利用できます
```ruby
    if basename =~ /\Ahtml_(.*)/
      "YML" + super($1, abspath)
```

# 最後に
これらを応用すればより柔軟なクラスやモジュール名の変更が可能です。
どうしてもconfigだけでの解決ができない場合検討してはいかがでしょうか？



# 参考:
- [Railsガイド](https://railsguides.jp/autoloading_and_reloading_constants.html)
- [Zitwerk](https://github.com/fxn/zeitwerk) 
- [Zeitwerkの壊し方](https://qiita.com/fursich/items/717a720d9f4465e4cbbb)
- [Zeitwerk を使いこなしたい](https://zenn.dev/megeton/articles/a3eac8f77fe1cd)