How do I validate members of an array field?

后端 未结 5 721
一整个雨季
一整个雨季 2020-12-14 10:30

I have this model:

class Campaign

  include Mongoid::Document
  include Mongoid::Timestamps

  field :name, :type => String
  field :subdomain, :type =&g         


        
相关标签:
5条回答
  • 2020-12-14 11:03

    You'll probably want to define your own custom validator for the emails field.

    So you'll add after your class definition,

    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
    

    The regex itself may not be perfect, but this is the basic idea. You can check out the rails guide as follows:

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

    0 讨论(0)
  • 2020-12-14 11:11

    Milovan's answer got an upvote from me but the implementation has a few problems:

    1. Flattening nested arrays changes behavior and hides invalid values.

    2. nil field values are treated as [nil], which doesn't seem right.

    3. The provided example, with presence: true will generate a NotImplementedError error because PresenceValidator does not implement validate_each.

    4. Instantiating a new validator instance for every value in the array on every validation is rather inefficient.

    5. The generated error messages do not show why element of the array is invalid, which creates a poor user experience.

    Here is an updated enumerable and array validator that addresses all these issues. The code is included below for convenience.

    # 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
    
    0 讨论(0)
  • 2020-12-14 11:15

    Found myself trying to solve this problem just now. I've modified Tim O's answer slightly to come up with the following, which provides cleaner output and more information to the errors object that you can then display to the user in the view.

    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
    
    0 讨论(0)
  • 2020-12-14 11:19

    You can define custom ArrayValidator. Place following in 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
    

    You can use it like this in your models:

    class User
      include Mongoid::Document
      field :tags, Array
    
      validates :tags, array: { presence: true, inclusion: { in: %w{ ruby rails } }
    end
    

    It will validate each element from the array against every validator specified within array hash.

    0 讨论(0)
  • 2020-12-14 11:23

    Here's an example that might help out of the rails api docs: http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates

    The power of the validates method comes when using custom validators and default validators in one call for a given attribute e.g.

    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
    
    0 讨论(0)
提交回复
热议问题