【问题标题】:How do I validate members of an array field?如何验证数组字段的成员?
【发布时间】:2011-04-14 20:50:10
【问题描述】:

我有这个模型:

class Campaign

  include Mongoid::Document
  include Mongoid::Timestamps

  field :name, :type => String
  field :subdomain, :type => String
  field :intro, :type => String
  field :body, :type => String
  field :emails, :type => Array
end

现在我想验证emails 数组中的每封电子邮件的格式是否正确。我阅读了 Mongoid 和 ActiveModel::Validations 文档,但没有找到如何执行此操作。

你能给我一个指针吗?

【问题讨论】:

  • 请注意,“验证”电子邮件地址有多种含义:您可以粗略地通过使用简单的正则表达式来确定地址格式是否正确,如答案所示,但是没有正则表达式可以涵盖电子邮件规范允许的所有极端情况。您还可以通过向该地址发送电子邮件来验证地址是否有效,如果已发送,则该地址被视为有效。这是这样做的首选方式,因为尽管电子邮件可能在语法上是正确的,但它可能不是“活着的”。
  • 同意。除了关于“没有正则表达式可以涵盖电子邮件规范允许的所有极端情况”的部分......这不是真的。这是一个普通的正则表达式,但它是可行的。

标签: ruby-on-rails ruby ruby-on-rails-3 mongodb mongoid


【解决方案1】:

您可以定义自定义ArrayValidator。关注app/validators/array_validator.rb:

class ArrayValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, values)
    Array(values).each do |value|
      options.each do |key, args|
        validator_options = { attributes: attribute }
        validator_options.merge!(args) if args.is_a?(Hash)

        next if value.nil? && validator_options[:allow_nil]
        next if value.blank? && validator_options[:allow_blank]

        validator_class_name = "#{key.to_s.camelize}Validator"
        validator_class = begin
          validator_class_name.constantize
        rescue NameError
          "ActiveModel::Validations::#{validator_class_name}".constantize
        end

        validator = validator_class.new(validator_options)
        validator.validate_each(record, attribute, value)
      end
    end
  end
end

你可以在你的模型中这样使用它:

class User
  include Mongoid::Document
  field :tags, Array

  validates :tags, array: { presence: true, inclusion: { in: %w{ ruby rails } }
end

它将根据array哈希中指定的每个验证器验证数组中的每个元素。

【讨论】:

  • 这很好用,谢谢!我要做的一个改变是添加一个条件来确保数组被填充。这样,如果您不需要该字段的存在,如果它是空白的,它将不会尝试验证它。否则,您会收到错误消息。所以,我的版本会将整个方法包装在 if values.present?
  • 为什么要扁平化?接受嵌套数组对我来说听起来像是一个错误。
  • Flatten 用于处理单个和类似数组的值(例如,传递单个标签,或传递标签数组)。自从我很久以前写过这篇文章以来,无法确切说明为什么这很重要,但有一个原因,我敢肯定:) 你对嵌套数组是正确的。
  • 应该是Array(values),而不是[values].flatten。如果给定数组,Array(arr) 将不会创建嵌套数组。这样一来,值不会被展平,并且验证器对于嵌套数组将失败。
  • 谢谢@lukad - 我同意你的看法。我已经更新了上面的代码。
【解决方案2】:

Milovan 的回答得到了我的支持,但实施存在一些问题:

  1. 扁平化嵌套数组会改变行为并隐藏无效值。

  2. nil 字段值被视为[nil],这似乎不对。

  3. 提供的示例,带有presence: true 将生成NotImplementedError 错误,因为PresenceValidator 没有实现validate_each

  4. 在每次验证时为数组中的每个值实例化一个新的验证器实例是相当低效的。

  5. 生成的错误消息未显示数组元素无效的原因,从而导致用户体验不佳。

这是一个更新的enumerable and array validator,它解决了所有这些问题。为方便起见,将代码包含在下面。

# Validates the values of an Enumerable with other validators.
# Generates error messages that include the index and value of
# invalid elements.
#
# Example:
#
#   validates :values, enum: { presence: true, inclusion: { in: %w{ big small } } }
#
class EnumValidator < ActiveModel::EachValidator

  def initialize(options)
    super
    @validators = options.map do |(key, args)|
      create_validator(key, args)
    end
  end

  def validate_each(record, attribute, values)
    helper = Helper.new(@validators, record, attribute)
    Array.wrap(values).each do |value|
      helper.validate(value)
    end
  end

  private

  class Helper

    def initialize(validators, record, attribute)
      @validators = validators
      @record = record
      @attribute = attribute
      @count = -1
    end

    def validate(value)
      @count += 1
      @validators.each do |validator|
        next if value.nil? && validator.options[:allow_nil]
        next if value.blank? && validator.options[:allow_blank]
        validate_with(validator, value)
      end
    end

    def validate_with(validator, value)
      before_errors = error_count
      run_validator(validator, value)
      if error_count > before_errors
        prefix = "element #{@count} (#{value}) "
        (before_errors...error_count).each do |pos|
          error_messages[pos] = prefix + error_messages[pos]
        end
      end
    end

    def run_validator(validator, value)
      validator.validate_each(@record, @attribute, value)
    rescue NotImplementedError
      validator.validate(@record)
    end

    def error_messages
      @record.errors.messages[@attribute]
    end

    def error_count
      error_messages ? error_messages.length : 0
    end
  end

  def create_validator(key, args)
    opts = {attributes: attributes}
    opts.merge!(args) if args.kind_of?(Hash)
    validator_class(key).new(opts).tap do |validator|
      validator.check_validity!
    end
  end

  def validator_class(key)
    validator_class_name = "#{key.to_s.camelize}Validator"
    validator_class_name.constantize
  rescue NameError
    "ActiveModel::Validations::#{validator_class_name}".constantize
  end
end

【讨论】:

  • 为这个点赞!
  • Sim,这太好了,谢谢!问题,有什么方法可以让错误信息出现在一个闪现消息中?当我尝试在表单中提交不可接受的项目时,而不是给我一个漂亮的错误消息,它会将我带到典型的活动记录错误页面。我想将错误信息放在一条消息中。
  • 我在这里创建了这个评论的问题:stackoverflow.com/questions/19913373/…
  • 我实际上得到了答案。还有一个问题,如何让错误消息包含所有无效条目?现在,前缀只能是数组中的一项。你要我单独提出这个问题吗?
  • 在 Rails 4.1 中,options 现在包含一个新的 :class 键 (github.com/rails/rails/blob/…),因此这会因以下错误而中断:uninitialized constant ActiveModel::Validations::ClassValidator。要修复,请从 initialize 方法的选项中删除该键。请参阅我对 Sim 要点的评论中的代码:gist.github.com/ssimeonov/6519423#comment-1246177
【解决方案3】:

您可能希望为电子邮件字段定义自己的自定义验证器。

所以你会在你的类定义之后添加,

validate :validate_emails

def validate_emails
  invalid_emails = self.emails.map{ |email| email.match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) }.select{ |e| e != nil }
  errors.add(:emails, 'invalid email address') unless invalid_emails.empty?
end

正则表达式本身可能并不完美,但这是基本思想。您可以按如下方式查看rails指南:

http://guides.rubyonrails.org/v2.3.8/activerecord_validations_callbacks.html#creating-custom-validation-methods

【讨论】:

    【解决方案4】:

    刚刚发现自己正在尝试解决这个问题。我稍微修改了 Tim O 的答案以提出以下内容,它为错误对象提供了更清晰的输出和更多信息,然后您可以在视图中向用户显示这些信息。

    validate :validate_emails
    
    def validate_emails
      emails.each do |email|
        unless email.match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
          errors.add(:emails, "#{email} is not a valid email address.")
        end
      end
    end
    

    【讨论】:

      【解决方案5】:

      下面是一个可能对 Rails api 文档有所帮助的示例:http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates

      validates 方法的强大之处在于在一次调用中使用自定义验证器和默认验证器来调用给定属性,例如

      class EmailValidator < ActiveModel::EachValidator
        def validate_each(record, attribute, value)
          record.errors[attribute] << (options[:message] || "is not an email") unless
            value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
        end
      end
      
      class Person
        include ActiveModel::Validations
        attr_accessor :name, :email
      
        validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
        validates :email, :presence => true, :email => true
      end
      

      【讨论】:

      • 谁曾否决过这个答案,请详细说明原因?我的回答直接来自关于如何验证电子邮件地址的官方 Rails 文档。
      • 再一次,如果你想对这个答案投反对票也没关系,但是开车经过,点击向下箭头,然后什么都不说对我没有任何帮助想知道这个特定答案有什么问题。这直接来自官方 Rails 文档!
      • 1) 发帖人专门询问 Mongoid 的验证。除非某些语法在 AR 和 Mongoid 中的工作方式相同,否则您的回答完全忽略了这一事实。 2)他在问如何验证 Mongoid 中单个数组字段的每个元素,期望每个元素都是有效的电子邮件地址。您已经展示了如何验证只能包含一个电子邮件地址的字符串字段。
      猜你喜欢
      • 1970-01-01
      • 2016-04-15
      • 1970-01-01
      • 2016-07-22
      • 1970-01-01
      • 1970-01-01
      • 2021-06-27
      • 2012-12-17
      • 2019-10-23
      相关资源
      最近更新 更多