[Rails] 特定のバリデーションに失敗したかどうかを判定する

## TL;DR

[ActiveModel::Errors#of_kind?](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-of_kind-3F) を使う。

```rb
# validates :diameter, numericality: { greater_than: 0.0, less_than_or_equal_to: 100 }
# の less_than_or_equal_to: 100 に失敗したかどうかを判定する。
yoyo = record_invalid_error.record
yoyo.errors
#=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=diameter, type=less_than_or_equal_to, options={value: 0.1001e3, count: 100}>]>

yoyo.errors.of_kind?(:diameter)
#=> false
yoyo.errors.of_kind?(:diameter, :greater_than)
#=> false
yoyo.errors.of_kind?(:diameter, :less_than_or_equal_to)
#=> true
```

## やりたいこと

以下のような Yoyo モデルがある。

```app/models/yoyo.rb
class Yoyo < ApplicationRecord
  # 直径 (mm)
  validates :diameter, numericality: { greater_than: 0.0, less_than_or_equal_to: 100 }
  # 幅 (mm)
  validates :width, numericality: { greater_than: 0.0, less_than_or_equal_to: 100 }
  # 重さ (g)
  validates :weight, numericality: { greater_than: 0.0, less_than_or_equal_to: 1_000 }
end
```

モデルの保存時に `ActiveRecord::RecordInvalid` エラーが発生した。ここで `diameter` の `less_than_or_equal_to: 100` というバリデーションに失敗したかどうかを判定したい。

例えばバリデーションのエラーメッセージで判定できる。

```rb
error
#=> #<ActiveRecord::RecordInvalid: Validation failed: Diameter must be greater than 0.0>

yoyo = error.record
yoyo.errors[:diameter]
#=> ["must be less than or equal to 100"]

# greater_than: 0.0 ではなく less_than_or_equal_to: 100 に失敗している。
yoyo.errors[:diameter].any? { it.include?('must be greater than') }
#=> false
yoyo.errors[:diameter].any? { it.include?('must be less than or equal to') }
#=> true
```

しかし、この方法には問題がある。エラーメッセージは変更可能なので、もしメッセージが変更されたらこの判定方法は使えなくなってしまう。

```config/locales/en.yml
en:
  activerecord:
    errors:
      models:
        yoyo:
          attributes:
            diameter:
              less_than_or_equal_to: must be <= %{count}

```

```rb
error
#=> #<ActiveRecord::RecordInvalid: Validation failed: Diameter must be <= 100>

yoyo = error.record
yoyo.errors[:diameter]
#=> ["must be <= 100"]

# エラーメッセージが変わったので、先ほどの判定方法は使えない。
yoyo.errors[:diameter].any? { it.include?('must be greater than') }
#=> false
yoyo.errors[:diameter].any? { it.include?('must be less than or equal to') }
#=> false
```

では、`diameter` の `less_than_or_equal_to: 100` というバリデーションに失敗したかどうかを判定するもっとよい方法はないだろうか？

## 方法

[ActiveModel::Errors#of_kind?](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-of_kind-3F) を使うと、どの属性のどの形式のバリデーションに失敗したかを厳密に判定できる。

```rb
yoyo.errors
#=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=diameter, type=less_than_or_equal_to, options={value: 0.1001e3, count: 100}>]>

yoyo.errors.of_kind?(:diameter)
#=> false
yoyo.errors.of_kind?(:diameter, :greater_than)
#=> false
yoyo.errors.of_kind?(:diameter, :less_than_or_equal_to)
#=> true
```

なお、属性 (diameter) だけで判定したい場合は [ActiveModel::Errors#include?](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-include-3F) を使う。

```rb
yoyo.errors.include?(:diameter)
#=> true
yoyo.errors.include?(:diameter, :less_than_or_equal_to)
# wrong number of arguments (given 2, expected 1) (ArgumentError)
```

## バージョン情報

```rb
RUBY_VERSION
#=> "3.4.1"
Rails.version
#=> "8.0.1"
```

## 参考

- Ruby on Rails API 
  - [ActiveModel::Errors](https://api.rubyonrails.org/classes/ActiveModel/Errors.html)
  - [ActiveModel::Error](https://api.rubyonrails.org/classes/ActiveModel/Error.html)