7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ActiveModel で ActiveRecord と同等のバリデーションを可能にする (before_type_cast)

Last updated at Posted at 2020-12-21

現状の動作

ActiveModelを利用して作成したフォームクラスが存在する

hoge_form.rb
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でも同様にエラーになることを期待する

調査

ActiveModelnumericality のバリデーションの実装のソースコードは[こちら]
(https://github.com/rails/rails/blob/master/activemodel/lib/active_model/validations/numericality.rb)

バリデーションの検証に使う値を特定するメソッドを引用

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 周りの機能が入っていないため、キャスト後の値でバリデーションを行っている

解決方法

ActiveModelbefore_type_cast の機能が入っていれば ActiveRecord と同等のバリデーションになりそう

ActiveRecordbefore_type_cast の機能を実装しているソースを探した
https://github.com/rails/rails/blob/2a0d4950398d1c5b5cd73186ac5269a9afa933e8/activerecord/lib/active_record/attribute_methods/before_type_cast.rb

このモジュールをフォームクラスでincludeする

hoge_form.rb
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::BeforeTypeCastinclude しても副作用が起きそうには見えなかったのですが
推奨されるものでは無さそうです(不具合の責任は取りません)

弊社コラビットではRailsエンジニアを募集しています
https://github.com/collabit-inc/job-offer-engineer

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?