rails/validatesでbefore_type_cast

(1389d) 更新

公開メモ

validates で type cast される前の値を検証したい

params で得たフォームデータは、 ActiveRecord へ代入される際に ActiveRecord の各 columns のデータ型にキャストされます。

例えば record.birthday がデータベース上で datetime 型の時、 params[:record][:birthday] の文字データは ruby の DateTime 方に変換されてから record.birthday に代入されます。

これは期待通りの結果と言えるのですが、 validates で指定した検証は通常、すでに DateTime 型に変換され、record.birthday へ代入された値に対して呼び出されます。

ですので、varidates で record.birthday に対して DateTime 型のメソッドを呼び出して検証するなどが可能になるわけです。

しかし、コントローラのロジックとしては、DateTime 型へ変換される前の、 Web Form で入力される文字列に対して検証を行いたい場合があります。

例えば、Web Form で birthday の入力が任意であったとします。 このとき該当の input 要素が空であれば、 予定通り record.birthday には nil が入力されて、問題ありません。

しかし、DateTime へ変換できない形式の文字列であった場合にも、 type cast 後の record.birthday は nil になってしまうため、 まったく文字列が入力されなかったのと見分けが付きません。

(attribute)_before_type_cast

こういう時のために、ActiveRecord では record.birthday と同時に、 record.birthday_before_type_cast という属性が自動的に定義されます。

http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html

これを使うと、例えば

LANG:ruby
  validates :birthday_before_type_cast, 
      format: { with: /\A(2014)-([01]\d)-([0-3]\d)\z/ }, 
                unless: ->(rec){ rec.birthday_before_type_cast.blank? }

などとして、type cast 前の値で検証を行えます。

エラーメッセージの問題

ただし上記のコードでは、検証失敗時に表示されるメッセージは、

Birthday before type cast is invalid

等となってしまい、ユーザーに提示するメッセージとしては不適切な物になってしまいます。

ここは、

Birthday is invalid

としてほしかった!

エラー表示の問題

エラーが起きた input 要素をマークする際にも、 not @record.errors[:birthday].empty? だけではエラーのあるなしを判別できず、 not @record.errors[:birthday].empty? or not @record.errors[:birthday_before_type_cast].empty? などとしなければならず、面倒きわまりないです。

ActiveRecord::Errors へのパッチ

この問題への対処としては、ActiveRecord::Errors::add において エラーメッセージを errors へ登録する際に、attribute の末尾が _before_type_cast であればその部分を削ってしまうのが良いように思いました。

ActiveRecord のソースに直接パッチを当てるのであれば、 https://github.com/rails/rails/blob/b97035df64f5b2f912425c4a7fcb6e6bb3ddab8d/activemodel/lib/active_model/errors.rb#L291 のあたりになりますが・・・

ちょっとそれも嫌だったので、とりあえず以下のモジュールを require してしのぐことにしました。

lib/active_model_errors_add_with_removing_before_type_cast_extention.rb

LANG:ruby(linenumber)
#
# (the_attribute)_before_type_cast を用いた validation において
# エラーメッセージが "The attribute before type cast is invalid" 
# となってしまったり、実際には the_attribute にエラーがあるにも
# かかわらず record.errors[:the_attribute].empty? になってしまったり
# といった不具合を除くため、errors への add 時に、attribute 名から
# _before_type_cast を取り除いてしまう荒っぽいパッチ
#
module ActiveModelErrorsAddWithRemovingBeforeTypeCastExtention
  def self.included(mod)
    mod.class_eval do
      alias_method_chain :add, :removing_before_type_cast
    end
  end

  def add_with_removing_before_type_cast(attribute, message = :invalid, options = {})
    # 末尾が _before_type_cast で終わっていて
    # なおかつ対応する attribute が存在すれば書き換える
    if attribute.to_s =~ /(.*)_before_type_cast\z/
      stripped = $1
      attribute = stripped.to_sym if @base.attributes.has_key? stripped
    end
   add_without_removing_before_type_cast(attribute, message, options)
  end
end

# ActiveModel::Errors へ include する
ActiveModel::Errors.class_eval do
  include ActiveModelErrorsAddWithRemovingBeforeTypeCastExtention
end

必要な model/the_model.rb にて require して使います。

たぶん何かもっとずっと良い方法があるんじゃないかとも思うのですが、 見つけられずに上記の方法に落ち着きました。

もしあればどなたか教えてください!

質問・コメント





Counter: 3801 (from 2010/06/03), today: 1, yesterday: 0