rails/validatesでbefore_type_cast のバックアップの現在との差分(No.2)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[公開メモ]]

* 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

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

ここは、

 Birthday 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 のあたりになりますが・・・

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

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

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

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

#article_kcaptcha
**sdf [#c4047e37]
>[[dfs]] (&timetag(2019-02-15T03:21:43+09:00, 2019-02-15 (金) 12:21:43);)~
~
df~

//

#comment_kcaptcha


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