如何使validates_inclusion_of的错误消息显示允许的选项列表?

我有一个validates inclusion:在我的模型中,但我认为“未包含在列表中”的默认错误消息完全没有用。

如何让它在错误消息本身中显示允许的选项列表? (例如, "is not one of the allowed options (option 1, option 2, or option 3)"

更具体地说,获得以下测试的最优雅方法是:

 describe Person do describe 'validation' do describe 'highest_degree' do # Note: Uses matchers from shoulda gem it { should allow_value('High School'). for(:highest_degree) } it { should allow_value('Associates'). for(:highest_degree) } it { should allow_value('Bachelors'). for(:highest_degree) } it { should allow_value('Masters'). for(:highest_degree) } it { should allow_value('Doctorate'). for(:highest_degree) } it { should_not allow_value('Elementary School'). for(:highest_degree).with_message('is not one of the allowed options (High School, Associates, Bachelors, Masters, or Doctorate)') } it { should_not allow_value(nil). for(:highest_degree).with_message('is required') } it { subject.valid?; subject.errors[:highest_degree].grep(/is not one of/).should be_empty } end end end 

,给出以下模型:

 class Person DegreeOptions = ['High School', 'Associates', 'Bachelors', 'Masters', 'Doctorate'] validates :highest_degree, inclusion: {in: DegreeOptions}, allow_blank: true, presence: true end 

这是我目前在config / locales / en.yml中的内容:

 en: activerecord: errors: messages: blank: "is required" inclusion: "is not one of the allowed options (%{in})" 

这是一个自定义validation器 ,它自动提供%{allowed_options}插值变量,以便在错误消息中使用:

 class RestrictToValidator < ActiveModel::EachValidator ErrorMessage = "An object with the method #include? or a proc or lambda is required, " << "and must be supplied as the :allowed_options option of the configuration hash" def initialize(*args) super @allowed_options = options[:allowed_options] end def check_validity! unless [:include?, :call].any?{ |method| options[:allowed_options].respond_to?(method) } raise ArgumentError, ErrorMessage end end def allowed_options(record) @allowed_options.respond_to?(:call) ? @allowed_options.call(record) : @allowed_options end def allowed_options_string(record) allowed_options = allowed_options(record) if allowed_options.is_a?(Range) "#{allowed_options}" else allowed_options.to_sentence(last_word_connector: ', or ') end end def validate_each(record, attribute, value) allowed_options = allowed_options(record) inclusion_method = inclusion_method(allowed_options) unless allowed_options.send(inclusion_method, value) record.errors.add(attribute, :restrict_to, options.except(:in).merge!( value: value, allowed_options: allowed_options_string(record) ) ) end end private # In Ruby 1.9 Range#include? on non-numeric ranges checks all possible values in the # range for equality, so it may be slow for large ranges. The new Range#cover? # uses the previous logic of comparing a value with the range endpoints. def inclusion_method(enumerable) enumerable.is_a?(Range) ? :cover? : :include? end end 

包含在config / locales / en.yml中:

 en: activerecord: errors: messages: restrict_to: "is not one of the allowed options (%{allowed_options})" 

你可以像这样使用它:

  DegreeOptions = ['High School', 'Associates', 'Bachelors', 'Masters', 'Doctorate'] validates :highest_degree, restrict_to: {allowed_options: DegreeOptions}, allow_blank: true, presence: true # => "highest_degree is not one of the allowed options (High School, Associates, Bachelors, Masters, or Doctorate)" 

或者有一个范围:

  validates :letter_grade, restrict_to: {allowed_options: 'A'..'F'} # => "letter_grade is not one of the allowed options (A..F)" 

或者使用lambda / Proc:

  validates :address_state, restrict_to: { allowed_options: ->(person){ Carmen::states(country) } 

欢迎评论! 您是否认为应该将这样的内容添加到Rails(ActiveModel)核心?

这个validation器有更好的名称吗? restrict_to_optionsrestrict_to

咦! 看起来Rails明确地排除了( except(:in) )我们在将参数传递给I18n之前传入的:in选项!

以下是来自activemodel / lib / active_model / validations / inclusion.rb的rails源:

 class InclusionValidator < EachValidator def validate_each(record, attribute, value) delimiter = options[:in] exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter unless exclusions.send(inclusion_method(exclusions), value) record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) end end end 

为什么这样做?

并不是说将原始数组内插到错误消息中是非常有用的。 我们需要的是一个字符串参数(由数组构建),我们可以直接插值。

当我将validates更改为此时,我设法让测试通过:

  validates :highest_degree, inclusion: { in: DegreeOptions, allowed_options: DegreeOptions.to_sentence(last_word_connector: ', or ')} }, allow_blank: true, presence: true 

并将en.yml更改为:

  inclusion: "is not one of the allowed options (%{allowed_options})" 

但是,通过两个不同的哈希键传递DegreeOptions是很难看的。

我的观点是validation器本身应该为我们构建该密钥(并将其传递给I18n以插入到消息中)。

那么我们有什么选择呢? 创建自定义Validator,Monkey补丁现有InclusionValidator,或者向Rails团队提交补丁……