I have this model:
class Campaign
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :subdomain, :type =&g
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
Milovan's answer got an upvote from me but the implementation has a few problems:
Flattening nested arrays changes behavior and hides invalid values.
nil
field values are treated as [nil]
, which doesn't seem right.
The provided example, with presence: true
will generate a NotImplementedError
error because PresenceValidator
does not implement validate_each
.
Instantiating a new validator instance for every value in the array on every validation is rather inefficient.
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
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
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.
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