アソシエーション先のカスタムエラーメッセージとしてbaseのエラーをアソシエーション元のfull_messagesで表示しようとするとおかしなことになる。
具体的には、
# parent has_one childの関係があるとし、(has_manyでもよいが便宜上has_oneにしてる)
# 今childにカスタムバリデーションエラーがあるとする。
class Child < ApplicationRecord
#...
def hoge_validation
errors.add(:base, :some_validation)
end
#...
end
# そのときparentからfull_messagesを呼ぶと...
parent.errors.full_messages
# => [ "Child basesome_validation_message" ]
# このように "モデル名 エラー属性" がエラーメッセージの先頭に含まれてしまう
# (これを避けたくてカスタムバリデーションにしたのに...)
# 直接childからfull_messagesを呼ぶと...
parent.child.errors.full_messages
# => [ "some_validation_message" ]
# 本来こうなって欲しい
なんやこれと思ってfull_messagesの実装を見ると...
# なるほどfull_messageでメッセージを生成している
def full_messages
map { |attribute, message| full_message(attribute, message) }
end
# attributeが:baseならただメッセージを返す。もしや...
def full_message(attribute, message)
return message if attribute == :base
attr_name = attribute.to_s.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
I18n.t(:"errors.format",
default: "%{attribute} %{message}",
attribute: attr_name,
message: message)
end
attributeが:baseならただメッセージを返す、というところでもしや...と思いerrorsを確認すると、
# parent.errors.detail
=> {:"child.base"=>[{:error=>:some_validation}]}
# parent.child.errors.details
=> {:base=>[{:error=>:some_validation}]}
アソシエーション元のparentからみると、childのbaseにセットされたエラーは:"child.base"
に変更されるため、full_message
の1行目でreturnされなくなっているようだ。
解決策を諸先輩に伺ったところ、localeファイルにbase属性を追加して、それを空文字にすればひとまず動きそうと分かった。
# parent.ja.yml
ja:
activerecord:
models:
parent: アソシエーション元
attributes:
parent:
base: '' # ここを空文字にするのがポイント
これで理想通りの動きになった。
full_message
の実装を見るに、 "%{attribute} %{message}"
の頭半分に空文字入ることで想定した表記になるのかな...でもそれだと空白が先頭に入って嫌だなと思ったところ、 gemとしてrails-i18nを入れておくと、rails-i18n自体が持っているlocaleファイルのja.ymlに
errors:
format: "%{attribute}%{message}"
とあって空白問題も解決。
gemを入れて無くとも自前のja.ymlに上記を追記すればOK.
とりあえずこれで対応することにするけど、なんとなくhackyでもやっとする。
参考
https://github.com/rails/rails/issues/19863
https://github.com/rails/rails/pull/20962
該当箇所の修正issueとprもあるがcloseされており、修正は結局出なかった模様...
背景が知りたい.