`NumericalityValidator`の`:in`オプションでProcとSymbolをサポート
NumericalityValidatorの:inオプションにProcとSymbolを渡せるようになり、バリデーション範囲を動的に決定できるようになりました。
背景
これまで NumericalityValidator の:inオプションは、Rangeオブジェクトのみを受け付けていました。範囲がレコードの状態や他の属性値に依存する場合でも、静的な範囲しか指定できず、動的な範囲バリデーションを実現するには独自のカスタムバリデータを実装する必要がありました。
:presenceや:lengthなど他のバリデータではすでにProcやSymbolによる動的オプションが広くサポートされており、:inオプションでの非サポートは一貫性の欠如でもありました。本PRはこのギャップを埋めています。
技術的な変更
check_validity!とvalidate_eachの2箇所に変更が加えられ、起動時の型チェックとバリデーション実行時の動的解決が分離されました。
check_validity!の変更(起動時チェック):
変更前は:inの値がRange以外の場合に即座にArgumentErrorを発生させていました。
# 変更前
unless value.is_a?(Range)
raise ArgumentError, ":#{option} must be a range"
end
変更後はProcとSymbolも受け付けるよう条件が拡張されています。
# 変更後
unless value.is_a?(Range) || value.is_a?(Proc) || value.is_a?(Symbol)
raise ArgumentError, ":#{option} must be a range, a symbol or a proc"
end
validate_eachの変更(バリデーション実行時):
バリデーション実行時に resolve_value を呼び出して動的解決し、解決後の値がRangeであるかを検証するガード節が追加されています。
# 変更後(RANGE_CHECKSブランチ内)
elif RANGE_CHECKS.include?(option)
option_value = resolve_value(record, option_value)
unless option_value.is_a?(Range)
raise ArgumentError, ":#{option} must return a range"
end
unless value.public_send(RANGE_CHECKS[option], option_value)
record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
end
resolve_valueはActive Modelが既に持つ汎用的なメソッドで、Procであればcall、Symbolであればレコードに対してsendを行います。これにより、バリデーション実行のたびにレコードの最新の状態を参照した動的な範囲評価が実現されます。
設計判断
型チェックを「起動時」と「実行時」の2段階に分けた点が本PRの重要な設計判断です。
check_validity!は定義時(クラスロード時)に呼ばれ、設定値の型がRange・Proc・Symbolのいずれでもない場合は早期にエラーを通知します。一方、ProcやSymbolが返す値の型チェックはvalidate_each内で実行時に行い、Range以外を返した場合には「:in must return a range」という明確なメッセージでArgumentErrorを発生させます。この2段階構造により、静的な誤設定は起動時に、動的な誤りはバリデーション実行時に、それぞれ適切なタイミングで検出されます。
既存のresolve_valueメソッドを再利用したことで、追加ロジックを最小限に抑えつつ、他のオプションと一貫した実装になっています。
まとめ
本PRにより、:inオプションへのProcとSymbolのサポートが加わり、他のバリデータオプションとの設計上の一貫性が確立されました。静的な範囲では表現できなかった「レコードの状態に依存した数値範囲バリデーション」を、カスタムバリデータを書くことなく宣言的に記述できるようになります。