Skip to content

Instantly share code, notes, and snippets.

@RobertAudi
Last active March 27, 2022 15:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RobertAudi/ca194cefc42fb554dd9fb90b9fd71c8f to your computer and use it in GitHub Desktop.
Save RobertAudi/ca194cefc42fb554dd9fb90b9fd71c8f to your computer and use it in GitHub Desktop.
Rails array validator
en:
activerecord:
errors:
messages:
array:
default: "has invalid values: %{invalid_values}"
absence: "has non-blank values: %{invalid_values}"
presence: "has blank values: %{invalid_values}"
format: "has invalid values: %{invalid_values}"
exclusion: "has reserved values: %{invalid_values}"
length: "has values with an invalid length: %{invalid_values}"
# Usage:
#
# validates :array_column, array: { length: { is: 20 }, allow_blank: true }
# validates :array_column, array: { numericality: true }
#
# It also supports sliced validation
#
# validates :array_column, array: { presence: true, slice: 0..2 }
class ArrayValidator < ActiveModel::EachValidator
I18N_SCOPE = "activerecord.errors.messages.array"
GENERAL_OPTIONS = %i[allow_nil allow_blank slice message].freeze
attr_reader :general_options
attr_reader :validators
def initialize(options)
super
# The initializer of the ActiveModel::Validator class (which is
# the superclass of ActiveModel::EachValidator) freezes the options
# attribute, so we need to duplicate it to clean it.
@options = self.options.deep_dup
@general_options = @options.extract!(*GENERAL_OPTIONS)
@validators = @options.each_key.with_object({}) do |validator_name, validator_instances|
validator_instances[validator_name] = validator_class(validator_name)
end
end
def validate_each(record, attribute, values)
errors = build_errors_hash
collection = Array(values)
collection.slice!(general_options[:slice]) if general_options[:slice]
options.each do |validator_name, validator_options|
validator = build_validator(validator_name, attribute, validator_options)
collection.each do |item|
next if item.nil? && general_options[:allow_nil]
next if item.blank? && general_options[:allow_blank]
validator.validate_each(record, attribute, item)
if record.errors.include?(attribute)
errors[validator_name][:invalid_values] << item
record.errors.delete(attribute)
end
end
end
errors.each do |type, details|
next if details[:invalid_values].blank?
invalid_values = details[:invalid_values].join(", ")
message = details.fetch(:message) do
default_message = I18n.t(:default, scope: I18N_SCOPE, invalid_values: invalid_values)
I18n.t(type, scope: I18N_SCOPE, invalid_values: invalid_values, default: default_message)
end
record.errors.add(attribute, type, message: message)
end
if record.errors.any? && general_options[:message].present?
record.errors.add(attribute, message: general_options[:message])
end
end
def check_validity!
unless options.is_a?(Hash)
raise ArgumentError, "expected an options Hash but got: #{options.inspect}"
end
if options.blank?
raise ArgumentError, "At least one validation must be specified"
end
end
private
def build_errors_hash
options.each_with_object({}) do |(validator_name, validator_options), errors_hash|
details = { invalid_values: [] }
if validator_options.is_a?(Hash) && validator_options.key?(:message)
details[:message] = validator_options[:message]
end
errors_hash[validator_name] = details
end
end
def validator_class(name)
name = "#{name.to_s.camelize}Validator"
name.constantize
rescue NameError
"ActiveModel::Validations::#{name}".constantize
end
def build_validator(validator_name, attribute, validator_options)
validator_args = { attributes: attribute }
validator_args.merge!(validator_options) if validator_options.is_a?(Hash)
validators.fetch(validator_name).new(validator_args)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment