Railsでboolean型のカラムのvalidationを行い型が異なる場合エラーとして返す方法

# 初めに
* ruby 2.3.1
* Rails 5.0.2
* `custom validator`や`type cast`の話が出てきますが、この投稿では深くは扱っていません

# 問題
* Web APIを開発していて、boolean型のカラムのvalidateを行いたく、ググってよくヒットする書き方を試した
* boolean型のアトリビュートに対して型違いの更新リクエストを行いエラーを期待したが、なぜか成功扱い、にもかかわらず値は変わらずで矛盾していた
* model内で`self.アトリビュート名`を使いリクエストした値を取得しようとしたが、なぜかリクエスト以前の値が取得できた（＝リクエストが正常に行われていないように見えた）


#### migrate
```ruby:db/migrate/20170703183050_create_my_models.rb
class CreateMyModels < ActiveRecord::Migration[5.0]
  def change
    create_table :my_models do |t|

      t.boolean :my_attribute, default: false, null: false

    end
  end
end
```

#### model
```ruby:app/model/my_model.rb
class MyModel < ApplicationRecord

  validates :my_attribute, inclusion: { in: [true, false]}

end
```

#### リクエストとレスポンス

```json:PATCHリクエストボディ
{ 
    "my_attribute": 1
}
```

```json:PATCHレスポンスボディ
{
    "my_attribute": false,
    "message": "更新が正常に完了しました。"
}
```


# 原因
* modelにリクエストが到達すると`type cast`が行われる
* 型違いの場合、リクエストされた値は破棄されエラーも出ない
* リクエストされた値が破棄された後の`self.アトリビュート名`の中にはリクエスト以前の値（=正常な値）が入っていた為、`validates`に合格していたっぽい <b>注釈[^1] </b>
 
# 解決策
* `before_type_cast`を使う
* `アトリビュート名_before_type_cast`と書くことでメソッドが呼び出せ、`type cast`される前の値を取得することが出来る
* ただ、これだけだと後述の余談の部分に書くような`message`になってしまうため`custom validator`と組み合わせた




#### model
```ruby:app/model/my_model.rb
class MyModel < ApplicationRecord

  validates :my_attribute, boolean_before_type_cast: true

end
```

* `validates :アトリビュート名, バリデーター名: true`の形で呼ぶ


### validator
```ruby:app/validator/boolean_before_type_cast_validator.rb

class BooleanBeforeTypeCastValidator < ActiveModel::EachValidator

  def validate_each(record, attr, value)
    value = record.send("#{attr}_before_type_cast")
    if !value.is_a?(FalseClass) && !value.is_a?(TrueClass) && !(value == 'false') && !(value == 'true')
      record.errors.add(:base, "#{attr.to_s.humanize}に設定出来ない値が指定されています")
    end
  end

end
```
* ファイル名とクラス名の関係性はrailsの規約に適合させる
* 引数の`record`にMyModelオブジェクトが入っている
* 引数の`attr`にmodelの`validates`で指定した`my_attribute`アトリビュートが入っている
* 引数の`value`は`type cast`後の値が入っている為使わない
* migrateにて`default: false`を指定している為、`my_attribute`の値指定が無い場合（例えばcreateリクエストの場合）は、string型の"false"が自動的に補完されてリクエストされ、dbにはboolean型で保存される
* `my_attribute`にboolean型の値が指定されたリクエストの場合でも、railsの仕様上string型の"true"または"false"がリクエストされ、dbにはboolean型で保存される。rails consoleで行ってもweb API経由で行っても同じ結果になる
* rspecとfactorygirlを使っている場合は注意が必要（後述）





#### リクエストとレスポンス


```json:PATCHリクエストボディ
{ 
    "my_attribute": 1
}
```

```json:PATCHレスポンスボディ
{
    "message": "更新に失敗しました。My attributeに設定出来ない値が指定されています"
}
```

# rspecとfactorygirlを使っている場合の注意
* factorygirlのfactoryメソッドにて`my_attribute`の値をboolean型で記述しているとboolean型でリクエストが行われる
* テストを成功させる為にはvalidatorの条件に`!value.is_a?(FalseClass) && !value.is_a?(TrueClass)`を入れるか、factoryにてstring型で値を指定する
* factorygirl以外でboolean型でリクエストが行われるケースがあるか不明な為、validatorの条件に`!value.is_a?(FalseClass) && !value.is_a?(TrueClass)`を追加しておく方が安全且つfactoryの記述時に型を意識しなくても済む


# 余談 - custom validatorを使わないとこんな感じのmessage

#### modelをこう書く
```ruby:app/model/my_model.rb
class MyModel < ApplicationRecord

  validates :my_attribute_before_type_cast, inclusion: { in: [true, false]}

end
```


```json:PATCHリクエストボディ
{ 
    "my_attribute": 1
}
```


```json:PATCHレスポンスボディ
{
    "message": "更新に失敗しました。My attribute before type castに設定出来ない値が指定されています"
}
```

* `validates :アトリビュート名_before_type_cast, inclusion: { in: [true, false] }`の形で呼ぶ

* アトリビュート名に`_before_type_cast`がくっついてしまう







# 参考にしたwebページ
[武内修＠筑波大 - rails/validatesでbefore_type_cast](http://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%2Frails%2Fvalidates%E3%81%A7before_type_cast)

* 上記のページが大変参考になりました
* というか`rails before_type_cast`でググっても1500件くらいしかヒットしない、ヤバい




# その他
* boolean型を検証してちゃんとエラーで返す方法はググっても殆ど情報が無かった。他の人はどうやっているんだろうか
* もっと別のやり方、綺麗な書き方、間違っている点など、ご指摘あれば是非お願い致します
* factorygirlにはハマりました。


# 追記

rails 6.0.3.2を使ってかなりプレーンに近い条件で再度試したところ、相変わらず型違いが検証されず通ってしまう結果となりました。
これは環境によるんでしょうか。なぞいです

バリデーション（is_adminカラムに注目ください）

<img width="536" alt="スクリーンショット 2020-08-02 19.33.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107536/1b7c5de4-eb58-14ec-cb30-de8c6ac10203.png">


リクエストとレスポンス

<img width="1100" alt="スクリーンショット 2020-08-02 19.30.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107536/02da25b2-2934-bd14-5d17-abbb2388db81.png">

[^1]: ここはよくわかっていないです。実際にはmigrateで`boolean: true`と書いた部分でvalidateが行われているだけで、`validates :my_attribute, inclusion: { in: [true, false]}`は無意味な記述になっていたんじゃないかと推測しています