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

更新


公開メモ

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

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

エラー表示の問題

エラーが起きた 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 のあたりになりますが・・・

ちょっとそれも嫌だったので、とりあえず以下のモジュールを 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

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

質問・コメント





Counter: 12163 (from 2010/06/03), today: 4, yesterday: 0