Skip to content

Instantly share code, notes, and snippets.

@ssimeonov
Last active March 22, 2022 14:48
Show Gist options
  • Save ssimeonov/6519423 to your computer and use it in GitHub Desktop.
Save ssimeonov/6519423 to your computer and use it in GitHub Desktop.
Enumerable and array validators for ActiveModel::Validations in Rails. Especially useful with document-oriented databases such as MongoDB (accessed via an ODM framework such as Mongoid).
# Syntax sugar
class ArrayValidator < EnumValidator
end
# 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] || 'is invalid')
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
@phlegx
Copy link

phlegx commented Feb 17, 2015

Thx @monfresh! This solves the problem on rails >= 4.1

Using the validator with ":on" or ":if" fails!!! Any idea?

Here an example:

validates :color, presence: true, array: { inclusion: { in: ['red', 'green'] } }, on: :update, if: :some_method

@phlegx
Copy link

phlegx commented Feb 17, 2015

This code works for me without using :if and :on (Rails >= 4.1):

class ArrayValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, values)
    [values].flatten.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

using:

validates :color, presence: true, array: { inclusion: { in: ['red', 'green'] } } #, on: :update, if: :some_method

# or

validates :color, array: { presence: true, inclusion: { in: ['red', 'green'] } } #, on: :update, if: :some_method 

@phlegx
Copy link

phlegx commented Feb 17, 2015

Rails (4.1.7) slices default keys from options: https://github.com/rails/rails/blob/44e6d91a07c26cfd7d7b5360cc9a8184b0692859/activemodel/lib/active_model/validations/validates.rb#L106

class ArrayValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, values)
    [values].flatten.each do |value|
      options.except(:if, :unless, :on, :strict).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

And here the code for this gist:

def initialize(options)
    super
    @validators = options.except(:class, :if, :unless, :on, :strict).map do |(key, args)|
      create_validator(key, args)
    end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment