date型の値の日付チェックで、1930/2/30のような不正な日付を除外する方法を記載する。
発生した問題
f.date_selectのようなプルダウン形式の場合、そのままではmodel層でのバリデーションが行えない。
理由
view層でf.date_selectのプルダウンによって年月日をそれぞれ取得する方法だと、年月日のデータがパラメータから渡ってくる場合、パラメータはparams[:hoge]["date(1i)"]、params[:hoge]["date(2i)"]、params[:hoge]["date(3i)"]に分割して格納される。
このparams[:hoge]をモデルに格納(具体的にはFuga.new(params))するタイミングで、1930年2月30日のような不正なデータに補正が自動でかかり、1930年3月2日とされてしまう。補正後のデータはデータ的には妥当なためバリデーションをすり抜けてしまう。
前提
年月日をparams[:hoge]["date(1i)"]、params[:hoge]["date(2i)"]、params[:hoge]["date(3i)"]としてばらばらに取得しチェックすれば対応可能。ただしこの方法だとコントローラ層での対応となるので出来ればやりたくない。
(年月日を入力する画面ごとに対応が必要なため、対応抜けが発生する恐れがある)
f.date_selectで年月日をそれぞれプルダウンで取得する方法ではどうにもならない場合、テキストフィールドに変更もやむなしとする。
(ただしテキストフィールドで入ってくる値が1930/2/30みたいな不正なデータだとチェック時に値が消し飛ばされてnilが入る)
色々試したこと
1. rails 日付や時刻の正当性を validation する
まさにやりたいことそのものだったが、バリデータの呼び出しをコントローラ層で行わなければならないので保留。
2. Rails 4.1 で validates_timeliness を使う
gemで日付の正当性をチェックする方法を検討し、validates_timelinessを導入。
それでもやっぱり解決しない。(1930年2月30日は3月2日としてスルー)
設定の仕方がダメだったのかもしれない。保留。
3. railsのコールバックを試す
1930年2月30日⇒3月2日の変換がかかるタイミングの前にバリデータが動かせないかとコールバックを試す。
(after_initializeなど)
いずれも失敗。そもそもコールバック使い方が違うと思う。これはこれで要学習。
4. rails/validatesでbefore_type_cast
Active records before_type_cast
最終的にこの方法で落ち着いた。
カスタムバリデータ内で<カラム名>_before_type_castで変換がかかる前の値(1930年2月30日のような値)を取得することでバリデーションを行うことが出来た。
実装するとこんな感じ。
validate :date_valid?
def date_valid?
date = date_before_type_cast
return if date.blank?
# YYMMDDで飛んでくる前提
y = birthday[0, 4].to_i
m = birthday[4, 2].to_i
d = birthday[6, 2].to_i
unless Date.valid_date?(y, m, d)
errors.add(:date, "日付の値が不正です")
end
end