ActiveRecordのEachValidatorを複製して狙ったカラムのみバリデーションする。

無職はじめての本格的な平日一週間を過ごして体が慣れてきた。

目的

フォームなどのリアルタイムバリデーションで、ActiveRecordに設定したバリデーターを使いたいがvalid?で全部やってしまうのはなんだか乱暴な気がしたので、必要なカラムだけでやりたかった。

前提となるモデル

class Model < ActiveRecord::Base
  validates :str, :txt, :bol,
    presence: true

  validate :validate_str

  def validate_str
    errors.add(:str, :validate_str) if str == '失敗する'
  end
end

バリデーターを得る

Model.validators
=> [#<ActiveRecord::Validations::PresenceValidator:0x007fbd8cc96c28 @attributes=[:str, :txt, :bol], @options={}>]
Model._validators
=> {:str=>[#<ActiveRecord::Validations::PresenceValidator:0x007fbd8cc96c28 @attributes=[:str, :txt, :bol], @options={}>], :txt=>[#<ActiveRecord::Validations::PresenceValidator:0x007fbd8cc96c28 @attributes=
[:str, :txt, :bol], @options={}>], :bol=>[#<ActiveRecord::Validations::PresenceValidator:0x007fbd8cc96c28 @attributes=[:str, :txt, :bol], @options={}>]}

カラムごとのバリデーターを利用するには_validatorsの方が取り回しが良さそうなのでそっちを使う。

バリデーターの使い方

validator.validate(record)

なのでゴー

validator = Model._validators[:str].first

model = Model.new
validator.validate(model)
model.errors.messages
=> {:str=>["can't be blank"], :txt=>["can't be blank"], :bol=>["can't be blank"]}

strだけ検査したいんだけど、設定時にまとめてるから一緒にやってくれてしまう。特定のカラムだけ検査したいという欲求なので、今回の場合はうれしくない。

ActiveRecord::Validations::PresenceValidator@attributesを増減させることで対称を絞ることができるが、3カラムすべて同じActiveRecord::Validations::PresenceValidatorのインスタンスを見ているので、よくない。

単一カラム用バリデーターとして複製する

validatesで使われるEachValidatorの初期化はこうなっている。

def initialize(options)
  @attributes = Array(options.delete(:attributes))
  raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
  super
  check_validity!
end

ので、@attributesに目的のカラムのみが入るようにして作成しなおす。

only_str = validator.class.new(validator.options.merge(attributes: :str))
=> #<ActiveRecord::Validations::PresenceValidator:0x007f09150060e8 @attributes=[:str], @options={}>

model = Model.new
only_str.validate(model)
model.errors.messages
=> {:str=>["can't be blank"]}

validateのは無視される

model = Model.new(str: '失敗する')
only_str.validate(model)
model.errors.messages
=> {}

validateで付与されるバリデーションはある種のコールバックであり、カラムに特定されていないのでこの方法では無視される。

valid?で乱暴に検査をするのもいいが、これを機にEachValidator化してみてはいかがでしょうか。(今日はslim検査したくてmmmpa/slim_validationとか作ってた)