Ruby on Rails で日付の前後関係に関するバリデーションをどうやって書こうか調べていたら,Rails 7 で,ComparisonValidator なるものが導入されたことを知った。
この記事は,ComparisonValidator の簡単な紹介と,私がハマった落とし穴について。
概要
ComparisonValidator は,モデルオブジェクトの属性値を他の値と比較するバリデーター。
Rails 7 より前でも,数値に関しては NumericalityValidator を使って
validates :amount, numericality: { greater_than: 100 }
みたいなことができていた。この例は,amount
の値が 100 よりも大きいことを要請する。
これを,数値以外の比較可能な値にまで広げよう,ということらしい。
文字列でも日付でもいいわけだ。
日付を固定値と比較するバリデーションは,たとえば
validates :start_date,
comparison: { greater_than_or_equal_to: Date.new(1900, 1, 1) }
のように書ける。
また,他の属性値との比較は,属性名をシンボルで与えて,
validates :end_date,
comparison: { greater_than_or_equal_to: :start_date }
というように書ける。この例は,終了日(end_date
)が開始日(start_date
)以降でなければならない,というバリデーションだ。
おお! これこそまさに求めていた機能。
Rails 6 までに無かったのが意外だったが。
Proc を与える場合の注意
ここまではよかったのだが,この先で一つの落とし穴にハマった。
やりたかったことは,「終了日(end_date
)が本日以降でなければならない」というバリデーション。
これを
validates :end_date,
comparison: { greater_than_or_equal_to: Date.today }
と書いてはダメなことは分かっていた。
Date.today
の値はバリデーションを行うタイミングで求める必要がある。
しかし,上記のコードだと,モデルクラスを作る段階で式 Date.today
を評価してしまうので,正しいバリデーションにならない。
どうすればいいかというと,Date.today
の値を返すような Proc オブジェクトを与えればいい。その Proc オブジェクトは,バリデーションを行うタイミングで評価される。
こういうことは,私が ComparisonValidator を知ることになったどっかのサイト(失念)に書かれていて,サンプルコードも載っていた。
こんなふうに書けばよい(?):
validates :end_date,
comparison: { greater_than_or_equal_to: -> { Date.today } }
Proc オブジェクトを,いわゆるラムダ記法 -> { }
で与えている。スマートだね。
た,確か,どっかのサイトにこういうサンプルが載ってたんだ。し,信じてくれ。
ところが,これでバリデーションをかけると,エラーメッセージが
"wrong number of arguments (given 1, expected 0)"
になる。えっ,ちょっ・・・,それ,どういうこと? バグってね?
エラーメッセージの i18n がおかしいのかとか,いろいろ調べて数時間を溶かした。
結論としては,Proc オブジェクトの arity(引数の個数)の問題であった。
与えた Proc オブジェクトが呼ばれるとき,モデルオブジェクトが渡されるのだ。
そのモデルオブジェクトを使うかどうかは自由なのだが,-> { }
で生成した Proc オブジェクトはラムダなやつなので,引数の不一致を許さない。
これが
wrong number of arguments (given 1, expected 0)
の原因であるらしい。
解決策は,
validates :end_date,
comparison: { greater_than_or_equal_to: Proc.new{ Date.today } }
のように〈非ラムダな Proc オブジェクト〉を渡してやるか,
validates :end_date,
comparison: { greater_than_or_equal_to: -> x { Date.today } }
のようにダミーの引数(ブロックパラメーター)を持たせてやるか。
ダミーの引数は x
とかよりも _
のほうがよいかもしれない。