[ACCEPTED]-How do I validate members of an array field?-mongoid
You can define custom ArrayValidator
. Place following 3 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 2 will validate each element from the array against 1 every validator specified within array
hash.
Milovan's answer got an upvote from me but 13 the implementation has a few problems:
Flattening 12 nested arrays changes behavior and hides 11 invalid values.
nil
field values are treated 10 as[nil]
, which doesn't seem right.The provided 9 example, with
presence: true
will generate aNotImplementedError
error because 8PresenceValidator
does not implementvalidate_each
.Instantiating a new 7 validator instance for every value in the 6 array on every validation is rather inefficient.
The 5 generated error messages do not show why 4 element of the array is invalid, which creates 3 a poor user experience.
Here is an updated 2 enumerable and array validator that addresses all these issues. The code 1 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
You'll probably want to define your own 5 custom validator for the emails field.
So 4 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 3 regex itself may not be perfect, but this 2 is the basic idea. You can check out the 1 rails guide as follows:
Found myself trying to solve this problem 5 just now. I've modified Tim O's answer slightly 4 to come up with the following, which provides 3 cleaner output and more information to the 2 errors object that you can then display 1 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
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.