現状の動作
ActiveModelを利用して作成したフォームクラスが存在する
class HogeForm
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::AttributeMethods
attribute :number, :integer
validates :number, numericality: { only_integer: true }
end
number
に対して整数の値以外が来た場合、バリデーションエラーにしたい
form = HogeForm.new
form.number = 'hoge'
form.valid?
# => true
form.number
# => 0
hoge
を代入しても内部でinteger
にキャストされてしまい、バリデーションエラーにならない
期待する動作
ActiveRecord
で同様のことを行うと、キャストする前の値に対してバリデーションをかけるため、エラーになる
ActiveModel
でも同様にエラーになることを期待する
調査
ActiveModel
の numericality
のバリデーションの実装のソースコードは[こちら]
(https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations/numericality.rb)
バリデーションの検証に使う値を特定するメソッドを引用
def prepare_value_for_validation(value, record, attr_name)
return value if record_attribute_changed_in_place?(record, attr_name)
came_from_user = :"#{attr_name}_came_from_user?"
if record.respond_to?(came_from_user)
if record.public_send(came_from_user)
raw_value = record.public_send(:"#{attr_name}_before_type_cast")
elsif record.respond_to?(:read_attribute)
raw_value = record.read_attribute(attr_name)
end
else
before_type_cast = :"#{attr_name}_before_type_cast"
if record.respond_to?(before_type_cast)
raw_value = record.public_send(before_type_cast)
end
end
raw_value || value
end
雑に読んだところ #{attribute}_before_type_cast
の実装があればその値を利用するようだ
ActiveModel
には before_type_cast
周りの機能が入っていないため、キャスト後の値でバリデーションを行っている
解決方法
ActiveModel
に before_type_cast
の機能が入っていれば ActiveRecord
と同等のバリデーションになりそう
ActiveRecord
で before_type_cast
の機能を実装しているソースを探した
https://github.com/rails/rails/blob/2a0d4950398d1c5b5cd73186ac5269a9afa933e8/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
このモジュールをフォームクラスでinclude
する
class HogeForm
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::AttributeMethods
# 追加
include ActiveRecord::AttributeMethods::BeforeTypeCast
attribute :number, :integer
validates :number, numericality: { only_integer: true }
end
動作確認
form = HogeForm.new
form.number = 'hoge'
form.number # cast 後の値
=> 0
form.number_before_type_cast # before_type_cast が使えている
=> "hoge"
form.valid?
=> false # invalidな状態
form.errors.messages
=> {:number=>["は数値で入力してください"]} # 数値でない(hoge)のでエラー
form.number = 1.5 # 小数を代入
=> 1.5
form.number # castされて1
=> 1
form.valid? # invalid
=> false
form.errors.messages
=> {:number=>["は整数で入力してください"]} # ちゃんとcast前の1.5に対してバリデーションされている
あとがき
あるべき論だとActiveModelにBeforeTypeCastが取り込まれるのが良いと思います
ActiveRecord::AttributeMethods::BeforeTypeCast
を include
しても副作用が起きそうには見えなかったのですが
推奨されるものでは無さそうです(不具合の責任は取りません)
弊社コラビットではRailsエンジニアを募集しています
https://github.com/collabit-inc/job-offer-engineer