Rails のバリデーションを特定のコンテキスト「以外」で実行させる


2014年 07月 25日

Rails のバリデーションには特定のコンテキストのときだけ実行させることができる on オプションが存在している。

validates :field1, presence: true
validates :field2, presence: true
validates :field3, presence: true, on: :admin

こうすると、 valid?(:admin) のときだけは全てのフィールドが必須入力となるが、そうでない場合は field1field2 のみ必須となり、 field3 は任意入力になる。なるほど、便利だ。

ところが、これを逆転させたいとき、即ち :admin コンテキストのとき「だけ」field3 を任意入力にしたいとなると、途端に面倒なことになる。

validates :field1, presence: true
validates :field2, presence: true
validates :field3, presence: true, on: [:create, :update, :context_foo, :context_bar]

おおう。これ、 :context_baz が増えたら、ここもメンテナンスせなあかんのか? ちょっとそれはなくない? このコードからは「:admin コンテキストの時だけ任意入力」という意図が全く読み取れない(そもそも :admin が出てこないではないか)ので、適切にメンテナンスするのは無理がある。

少しだけマシな方法

で、Rails の実装を確認してみると、どうやら on オプションは if に読み替えて実装されているらしい。

def validate(*args, &block)
  options = args.extract_options!
  if options.key?(:on)
    options = options.dup
    options[:if] = Array(options[:if])
    options[:if].unshift lambda { |o|
      Array(options[:on]).include?(o.validation_context)
    }
  end
  args << options
  set_callback(:validate, *args, &block)
end

なるほど。では unless にすれば意味が反転するはずだ。

validates :field1, presence: true
validates :field2, presence: true
validates :field3, presence: true, unless: proc { [:admin].include?(validation_context) }

できた。若干・・・というか大分面倒ではあるが、将来増えるかもわからないコンテキストをすべて列挙するよりは、余程現実的だろう。

off とか not_on のようなオプションで標準サポートされていても良さそうなものだが、何か課題や Rails 哲学に反する点でもあるのだろうか。そもそもとして、このようなことが必要になるのはモデルの設計が悪いとか、コンテキストの使い方を間違っているとか、そういう話なのかもしれないが、そうは言っても現実というものは理想とは程遠いもんである。