RubyとRailsのバージョンアップで苦労したこと（Gem編）

依頼いただいて久しぶりに言語とFWのバージョンアップの対応を行ったので、苦労した点などを。

PHP(4->5)などのバージョンアップぐらいしか経験がなかったので不安でしたが、なんとかなりました。

### バージョン情報

#### 元のバージョン

- Ruby
    - 2.7.4
- Rails
    - 6.1.4

#### アップしたバージョン

- Ruby
    - 3.2.2
- Rails
    - 7.1.3

### やる前に想定していたこと

- 最近参画したプロダクトで知らない箇所しかないので苦労するだろうなぁ
- メジャーバージョンを上げるから、コードの書き方が変わる箇所も多々あるだろうなぁ
    - メソッドとかのDeprecateのWarningとかもすごいだろうなぁ
    - 設定とかも大きく変わるだろうから、精査しないといけないな
- Gemとかそのままだと動かないものがあるからバージョンアップ必要だろうなぁ
    - Gemもそもそも更新止まっていて、更新できないものもあるんじゃないかなぁ
- リリース後にトラブルがあった時に戻すの大丈夫かな

### いざ開始！そして見えてくる苦労

幸いなことに
Ruby2.7.4 -> 3.2.2でアプリケーションのコードはそれほど大きく変わりませんでした

`to_s(:delimiter)`などが`to_formatted_s(:delimiter)`に変わったとか、
ハッシュ引数の渡し方が変わったとか
```rb
tt(str, options = {})
end

options = { a: 1, b:1 }
tt(str, options)
# ではなく

tt(str, **options)
```

そんな程度でした

### いざGemのアップデート！

今後のことを考えて、今回は全体的にgem updateを実行して、なるべく最新にしていきました。

その時、当然アプリケーションコードそのままだと動かないとか、設定方法が変わるとかは想定していましたが、**想定以上に時間がかかってしまいました**

#### Ransack5系からransackable_attributesの設定が必要

Ransackの検索項目が自由すぎたせいか、検索とソートできる内容を制限する機能が追加されました。
paramsのpermitのような設定ですね。

```rb
  def self.ransackable_attributes(auth_object = nil)
    ["id", "memo", "name", "updated_at"]
  end
```

乱暴にやると、

```rb
  def self.ransackable_attributes(auth_object = nil)
    # 全attributesを検索対象にするよ！という設定
    authorizable_ransackable_attributes
  end
```
で今まで通りの挙動になりますが、確かに制限はあったほうが良いかなと思い、全モデルに設定していきました。

しかし、これを行わないと、エラーが発生し、画面確認ができないので、

- 検索している箇所を特定する
- 検索しているカラムを特定する
- 検索に使用されているカラムを設定していく

という工程を全画面で確認しつつ登録していくことに・・・。

検索はまだエラーがでるので良いのですが、**ソートは設定されていなくてもエラーが出ず**、最初ソートも対象だと気が付かず検索のみ対象に行っていたところ、後ほどソートができないことがわかり
- ソート箇所を使用している箇所を特定する
- ソートしているカラムを特定する
- ソートで使用されているカラムを設定していく

という二度手間な作業となりました。
管理画面系でテーブルデータが多く、検索やソートも多用されているので、辛かったです。
一通り追加後、確認していきましたが、多分ソート効かない箇所があるんではないかと思っています。

また、関連するモデルでの検索を使用する場合は、 `ransackable_associations`で検索対象とするモデルの指定も必要です。

#### Deviseでユーザー招待ができなくなった！

次にユーザーの編集や新規ユーザー追加をしていて確認していたところ、
ユーザー追加を行うとエラーやメッセージが何も出ず、編集画面に戻ってくるという症状が出ました。
Deviseの問題なのか、別の問題なのか、拡張を外したりしつつ確認していると、
[DeviseInvitable](https://github.com/scambra/devise_invitable)関連の一部である`validate_on_invite: true`を外した時に動くことを確認できました。

`validate_on_invite: true` はメールアドレスのバリデーションを行ってくれる機能で、
メールアドレスが不正であれば送信されないという仕組みです。

deviseとの問題か？など設定方法が変わったのか、などを調べていくと、**パスワードがないからバリデーションエラーになる**という仕様になったようです。

いや、招待の時パスワードいらなくない？
と思いながら、

invitations_controller.rbで
```rb
  protected

  def invite_resource(&block)
    # パスワードをinvite_paramsハッシュに追加(Deviseのバリデーションで弾かれるため）
    params = invite_params.to_h
    params[:password] = SecureRandom.base64
    resource_class.invite!(params, current_inviter, &block)
  end
```
と、無理やり初期パスワードを設定して対応しました。
[こちらの記事](https://qiita.com/jnchito/items/689a7069f02ce14d1a3f)に救われました。

突然ユーザー追加だけできなくなり、最初は全く原因がわからなかったので焦りました。

#### CarrierWaveでアップロードした画像 xxx.jpgが、xxx(2).jpg となる

アップロードした画像がなんかおかしいので、調べていたら、
`xxx.jpg`をアップロードしたのに、画像名が`xxx(2).jpg`になってしまう問題が発生しました。

どうも重複した画像を上げる際に自動で接尾辞をつけてくれるようになったのですが、
問題としては**重複した画像などない**という事です。

オンリーワンかつナンバーワンの画像たちをアップロードしても、**(2).jpg**としてくれるのです。

issueやPRなどを見ても全くわからず、
バージョンを上げたり下げたりすると、**直る時がある**となり、バージョンの問題と思いました。

3.0.0以降ではアップロードされたファイルに接尾辞がついてしまうのです。
[Cange log](https://github.com/carrierwaveuploader/carrierwave/releases/tag/v3.0.0)を見てみると、それっぽい内容がありました

```
Support adding suffix to filename on store when path collides with the existing ones (@mshibuya 07a5632, #1855)
```

どうしたら直るのか、悩みながら、該当箇所のアップローダの設定を、ふと何の気もなしに

```rb
class Image< ApplicationRecord
  #....
  # belongs_toやvalidateなど、色々な設定
  #....
  mount_uploaders :images, ImageUploader
```
とされていた箇所を、上に上げてみました。

```rb
class Image< ApplicationRecord
  mount_uploaders :images, ImageUploader
```

これによりアップロードされる画像は `xxx.jpg`と正常になりました。

~~以前の処理では画像名の重複チェックのバリデーションとかが上にあったので、それらとの兼ね合いなのかなと考えながら、こちらは無事、**迷宮入りとなりました。**~~

迷宮入りさせようかと思ったのですが、もう少し調べてみると

https://github.com/carrierwaveuploader/carrierwave/issues/2682

と同時アップロード時の仕様だよとのことだったのですが、１枚でアップロードしても `xxx(2).jpg`となるので、やはりvalidationかattributesとかの兼ね合いっぽいのと、 `ImageUploader`に

```rb
class ImageUploader < CarrierWave::Uploader::Base
  ...

  def deduplicated_filename
    filename
  end
end
```

とすることで、重複ファイル名は解消できました。


#### FakerとPublicSuffixの戦い

とある処理で、URLを登録するという箇所があり、
RSpecで検証していた時に、RSpecの失敗する箇所がありました。

URL登録が失敗するんです。

ただ、該当箇所を画面で確認してみると、すんなり登録できてしまい、原因がわかりませんでした。

バリデーションの問題だろうか、と思い、`save`を`save!`に変えたところ、想定通り`URLが不正です`とバリデーションで失敗していて登録ができない模様です。
ではURLが問題なのだろうか、とURLの確認をしてみると、 `https://hogehgoe.example`や`https://hogehoge.to`など、特に問題なさそうな感じがしました。

うーむ、なぜだろう、とバリデーション箇所を調べていると、

```

  def validate_each(record, attribute, value)
    return if value.blank? || (PublicSuffix.valid?(value, default_rule: nil) && EXCLUDE_CARACTERS.all? { |caracter| value.exclude?(caracter) })
    record.errors.add(attribute, 'error_dayo')
  end
```

と特に問題なさそうな気が

ただ、`PublicSuffix`を知らなかったので、こちらを調べてみると、
[Public Suffix for Ruby](https://github.com/weppos/publicsuffix-ruby)というGemのようです。

こちらはドメインパーサーで、パースしたドメインが有効か無効かを判定してくれるありがたい機能のようです。

しかしこちらにドメインを入れてみると
```rb
value = 'hogehgoe.example'
[5] pry(main)> PublicSuffix.valid?(value, default_rule: nil)
=> false
```

と**このドメインは有効ではないよ！**という判定になりました。

今まで通っていたのに、なぜ。。。

そもそもこの、`default_rule: nil` とは一体・・・？

試しに、`default_rule: nil`を外してみると、

```rb
value = 'hogehgoe.example'
[5] pry(main)> PublicSuffix.valid?(value)
=> true
```

`true`になりました！
これにて一件落着！

ともいかず、原因を追ってみると、 `default_rule: nil`
は
`Strict validation (without applying the default * rule)`と、厳格なルールの指定のようです。

**厳格なルールとは・・・？** とコードを追ってみると


```
  # @example Parse an invalid (unlisted) domain with strict checking (without applying the default * rule)
  #   PublicSuffix.parse("x.yz", default_rule: nil)
  #   # => PublicSuffix::DomainInvalid: `x.yz` is not a valid domain
  
default_rule: list.default_rule
```

のように、引数があり且つ、nilである場合は、こちらのリストを使用してチェックするよ！というような内容の処理がありました。

**リストとは・・・？**
とさらにコードを追っていくと、`data` ディレクトリに存在していて、確かにトップレベルドメイン(comとかjpとか）の一覧があるではありませんか！

ここに、当たり前ですが、`example` などは入っておらず、バリデーションエラーとなったようです。

しかし**今までは動作していたテスト**なので、
そうなると、以前使っていたバージョン、2.0.0の使用していたバージョンには`example`が存在していたのでは！と思ったのですが、**別にありませんでした**

なぜ動いていたのかは永遠の謎として墓場に持って行こうと決意し、
RSpecで使用するURLを生成している箇所を確認すると

```
link_url { Faker::Internet.url }
```
となっていました。

この`Faker::Internet.url`は [Faker](https://github.com/faker-ruby/faker)というGemで
架空のURLを生成してくれたり、色々と偽のデータを作成してくれるとて便利なGemのようです

ただ、今回の問題となっているURL生成の際に `example`などの存在しないドメインで生成してしまうようです。

しかし、ホスト名は指定できるようで

```
link_url { Faker::Internet.url(host: 'faker-domain.com') }
```

などにすると、 `https://hogehoge.faker-domain.com` と生成されるURLのホスト名が指定できるので無事、テストが通るようになりました。

こうして、FakerとPublicSuffixの戦いは私の一人負けで幕を閉じました。


以上がGem関連で苦労した点です、他にも発見できず、表に出ていないだけで、隠れGem設定ミス＆変更していて問題になっているなどが存在しているかもしれません。

しかし全てのGem使用箇所などをチェックは到底不可能なので、今回は全て１００％達成というよりは、

- 事業としてクリティカルな箇所については動作するかどうかをしっかり確認する
- 今後発覚する問題についてはアップデートで対応する
- 問題が大きいようであればロールバックする

という形で進めているため、今後更新する可能性があります。