[Rails] ActiveModel::Errors#added? でコールバックオプション付きのクエリが正しく動作しない問題を修正
背景
RailsのActiveModelでは、バリデーションエラーを追加する際に allow_nil、if、unless などのコールバックオプションを指定できます。しかし、これらのオプション付きでエラーを追加した後、同じオプションを指定して added? メソッドでエラーの存在を確認すると、エラーが存在するにもかかわらず false が返される不具合がありました。
この問題は #54483 で報告されており、開発者がバリデーションの動作を正確に確認できない状況を引き起こしていました。
技術的な詳細
問題の原因
不具合の原因は ActiveModel::Error#strict_match? メソッドの実装にありました。このメソッドは、保存されているエラーからコールバックオプションとメッセージオプションをフィルタリングしていましたが、クエリ側のオプションからはフィルタリングしていませんでした。
変更前:
def strict_match?(attribute, type, **options)
return false unless match?(attribute, type)
options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
end
このコードでは、右辺の @options(保存されているエラーのオプション)からはコールバックオプションが除外されていますが、左辺の options(クエリのオプション)からは除外されていません。そのため、以下のような比較が行われていました:
# 実際の比較
{ allow_nil: true } == {} # => false
修正内容
修正では、両辺のオプションから同様にコールバックオプションとメッセージオプションをフィルタリングするように変更されました。
変更後:
def strict_match?(attribute, type, **options)
return false unless match?(attribute, type)
options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS) == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
end
これにより、両辺が対称的に比較されるようになりました:
# 修正後の比較
{} == {} # => true
使用例
修正後は、コールバックオプション付きのエラーチェックが正しく動作します:
# コールバックオプションのみ
errors.add(:name, :too_long, allow_nil: true)
errors.added?(:name, :too_long, allow_nil: true) # => true(修正前: false)
# コールバックオプションと他のオプションの組み合わせ
errors.add(:name, :too_long, count: 25, allow_nil: true)
errors.added?(:name, :too_long, count: 25, allow_nil: true) # => true
errors.added?(:name, :too_long, count: 25) # => true(コールバックオプションは無視される)
# メッセージオプション
errors.add(:name, :too_long, message: proc { "foo" })
errors.added?(:name, :too_long, message: proc { "foo" }) # => true
影響範囲
この修正により、以下のようなシナリオで正しい動作が保証されます:
-
条件付きバリデーション:
ifやunlessオプションを使用したバリデーションのテスト -
null許容バリデーション:
allow_nilやallow_blankオプションの動作確認 - カスタムメッセージ: Procを使用した動的メッセージの検証
コールバックオプションやメッセージオプションは、エラーの「種類」を識別するための情報ではなく、バリデーションの「実行条件」や「表示方法」に関する情報です。そのため、added? メソッドでのエラーチェックではこれらのオプションを無視することが適切な設計となります。