Last active
March 27, 2022 15:33
-
-
Save RobertAudi/ca194cefc42fb554dd9fb90b9fd71c8f to your computer and use it in GitHub Desktop.
Rails array validator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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