validates :number, numericality: {in: 3..9} なんて使えないぞ？

# はじめに

ActiveRecord のバリデーションで，数値が一定範囲に収まっていることを検証したいとする。

たとえば，Item クラスの `number` カラムの値が 3 以上 9 以下であることを検証するのに，昔は

```rb
class Item < ApplicationRecord
  validates :number, 
    numericality: {
      greater_than_or_equal_to: 3,
      less_than_or_equal_to: 9
    }
end
```

などと書いた。
こんな長ったらしい書き方はしたくないよね。

それが Rails 6.1 からは

```rb
class Item < ApplicationRecord
  validates :number, numericality: {in: 3..9}
end
```

と書けるようになった（はずだった）。
いやー，実に良いですな。

このことは，以下の記事でも紹介されている。

https://techracho.bpsinc.jp/hachi8833/2021_03_04/104474

Rails ガイドの「numericality」のところにも書かれている：

https://railsguides.jp/active_record_validations.html#numericality

ところが実際やってみると，検証がスルーされてしまう。
どうなってんの？

## 追記 2023-06-03

@jnchito さんのコメントですべて氷解。

`numericality` で `in` オプションが使えるようになったのは Rails 6.1 ではなく Rails 7.0 だった（正確には 7.0.0.alpha1 から）。

TechRacho さんの記事はバージョンに関しては誤り（翻訳元の記事がまちがっていr）。

Rails ガイドについては，上記のリンクは最新版についてのもので，現時点では Rails 7.0 について書かれている。
Rails 6.1 版は無料では見られないが，英語原文は見ることができ，ここには `in` について（当然）書かれていない。

なお，この機能は activemodel のほうで実装されているのだった（もちろん activerecord で使える）。

# 再現コード

実験は，Rails を使わず ActiveRecord 単体で可能だ。

activerecord 6.1 と sqlite3 がインストールされた環境で以下のコードを実行してみてほしい。

```rb
gem "activerecord", "~> 6.1"
require "active_record"
require "pathname"

db_path = Pathname(__dir__) / "db.sqlite3"
db_path.delete if db_path.exist?

ActiveRecord::Base.establish_connection adapter: "sqlite3",
  database: db_path,
  pool: 5,
  timeout: 5000

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

class CreateItem < ActiveRecord::Migration[6.1]
  create_table :items do |t|
    t.integer :number
  end
end

class Item < ApplicationRecord
  validates :number, numericality: {in: 3..9}
end

item = Item.new number: 10

p item.valid?
# => true

p item.errors.full_messages
# => []
```

`3..9` に入ってないといけないところへ `10` を与えているのに，検証にパスしている（valid になっている）。もちろんエラーメッセージも空。

なんでやねん。

## 実験(1)：greater_than_or_equal_to などを使う

バリデーションの書き方を，従来のものに変えてみよう：

```diff
-  validates :number, numericality: {in: 3..9}
+  validates :number, numericality: {
+    greater_than_or_equal_to: 3,
+    less_than_or_equal_to: 9
+  }
```

すると

```rb
p item.valid?
# => false

p item.errors.full_messages
# => ["Number must be less than or equal to 9"]
```

のように，ちゃんと検証できる。

## 実験(2)：activerecord 7.0

activerecord のバージョンを変えてみたらどうか。7.0 にしてみよう。

```diff
-gem "activerecord", "~> 6.1"
+gem "activerecord", "~> 7.0"
```

```diff
-class CreateItem < ActiveRecord::Migration[6.1]
+class CreateItem < ActiveRecord::Migration[7.0]
```

※`validates` の書き方は `in` を使ったほうに戻しておく。以下，常に最初のコードに対する差分を提示することにする。

実験に使った環境では activerecord 7.0.5 がインストールされていたので，実験は 7.0.5 だ。

これだとエラーが出る：

```
/Users/XXXX/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/activemodel-7.0.5/lib/active_model/validations/numericality.rb:53:in `public_send': undefined method `in?' for 10:Integer (NoMethodError)

            unless value.public_send(RANGE_CHECKS[option], option_value)
                        ^^^^^^^^^^^^
```

`10` に対し，`in?` というメソッドを呼ぼうとして NoMethodError が出ている。

どういうこっちゃ？

### 追記 2023-06-03

activerecord 7.0 で `numericality` の `in` オプションを使うには，activesupport が必要だった。

最低限

```rb
require "active_support/core_ext/object/inclusion"
```

を追加してやれば，期待どおりに `in` が使える。
つまり，

```rb
p item.valid?
# => false

p item.errors.full_messages
# => ["Number must be in 3..9"]
```

といった結果になる。
なお，`"active_support/core_ext/object/inclusion"` が面倒なら

```rb
require "active_support/all"
```

とすればよい。


## 実験(3)：`in` を他のものに変えてみる

activerecord 6.1 で，なぜ `in` が効かないのだろう。
もしかして，このオプション（`in`）を知らないのでは？

試しに，（たわむれに）`in` を `honyarara` にしてみよう：

```diff
-  validates :number, numericality: {in: 3..9}
+  validates :number, numericality: {honyarara: 3..9}
```

はい，

```rb
p item.valid?
# => true

p item.errors.full_messages
# => []
```

のように，バリデーションにパスしちゃいました！

NumericalityValidator は**オプション名のチェックなんぞしていない**のだ。
てことは，1 文字のタイポでも，「検証されるべきが検証されない」バグになるわけだ。モデルのテストを網羅的に書かないとヤベェぞ。
マジかよ？
`less_than_or_equals_to` を `less_then_or_equals_to` と書いたらアウトだぞ？

このことは，activerecord 7.0 でも同じだった。
`in` についてはエラーが出たが，`honyarara` とかだと出ない。

# ドキュメント

本件についてどこかに何か書いてないかと思い，いろいろ見てみたが，探し方が悪いのか何も見つけられなかった。

# どうすれば

結局，どう書けばいいのだろうか。
可能なら `greater_than_or_equal_to` の類は使いたくないぞ。

どうやら

```rb
  validates :number, inclusion: {in: 3..9}
```

でいいようだ。
NumericalityValidator とちゃうんかいっ！

今ひとつ釈然としないが，どう書けばよいかだけは分かった。

## 追記 2023-06-03

結論：

* `numericality` で `in` オプションが使えるのは activemodel 7.0 から
* activesupport も必要（Rails なら意識しなくてよい）

感想：

* 疲れた（何時間も溶かした）