rails/validatesでbefore_type_cast のバックアップソース(No.1)

更新

[[公開メモ]]

* validates で type cast される前の値を検証したい [#rf63f1c9]

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 [#i013c0ad]

こういう時のために、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 前の値で検証を行えます。

* エラーメッセージの問題 [#bf54ddb8]

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

 Birthday before type cast is invalid

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

* エラー表示の問題 [#lcd24791]

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

* ActiveRecord::Errors へのパッチ [#u6a430e8]

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

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

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

 LANG:ruby(linenumber)
 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 = {})
     if attribute.to_s =~ /(.*)_before_type_cast\z/
       stripped = $1.to_sym
       attribute = stripped if attributes.has_key? stripped
     end
    add_without_removing_before_type_cast(attribute, message, options)
   end
 end
  
 ActiveModel::Errors.class_eval do
   include ActiveModelErrorsAddWithRemovingBeforeTypeCastExtention
 end

もっと良い方法もありそうですが・・・
もしあれば教えてください。。。

* 質問・コメント [#wad1a837]

#article_kcaptcha

Counter: 12163 (from 2010/06/03), today: 1, yesterday: 3