Skip to content

Instantly share code, notes, and snippets.

@phlegx
Forked from ssimeonov/array_validator.rb
Last active May 5, 2021 18:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phlegx/73c3a74701aa047dd913 to your computer and use it in GitHub Desktop.
Save phlegx/73c3a74701aa047dd913 to your computer and use it in GitHub Desktop.
Rails 6.1 array validator with indexed messages.
# 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.except(:class, :if, :unless, :on, :strict).map do |(key, args)|
create_validator(key, args)
end
end
def validate_each(record, attribute, values)
helper = Helper.new(@validators, record, attribute, options)
errors = []
Array.wrap(values).each do |value|
errors << helper.validate(value)
end
# Remove existing errors.
errors.flatten.each do |error|
if record.errors.where(attribute, error[:error]).length
record.errors.delete(attribute, error[:error])
end
end
# Add all new attribute errors.
errors.flatten.each do |error| # rubocop:disable Style/CombinableLoops
# Add all new attribute errors.
record.errors.add(attribute, error.delete(:error), **error)
end
end
private
# Helper class.
class Helper
def initialize(validators, record, attribute, options)
@validators = validators
@record = record
@attribute = attribute
@options = options
@count = -1
end
def validate(value)
@count += 1
errors = []
@validators.each do |validator|
next if value.nil? && validator.options[:allow_nil]
next if value.blank? && validator.options[:allow_blank]
result = validate_with(validator, value)
errors << result if result
end
errors
end
def validate_with(validator, value)
before_errors = error_count
run_validator(validator, value)
return unless error_count > before_errors
prefix = "element #{@count} (#{value}) "
errors = []
(before_errors...error_count).each do |pos|
errors[pos] = error_details[pos].merge(
message: prefix + (error_messages[pos] || 'is invalid'),
value: value
)
end
errors.last
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_details
@record.errors.details[@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.is_a?(Hash)
validator_class(key).new(opts).tap(&:check_validity!)
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment