lib/volt/models/validators/format_validator.rb
module Volt
# Validates the format of a field against any number of block or regex
# criteria
class FormatValidator
# Creates a new instance with the provided options and returns it's errors
#
# @example
# options = { with: /.+@.+/, message: 'must include an @ symobl' }
#
# FormatValidator.validate(user, 'email', options)
#
# @example
# numbers_only = /^\d+$/
# sum_equals_ten = ->(s) { s.chars.map(&:to_i).reduce(:+) == 10 }
#
# options = [
# { with: numbers_only, message: 'must include only numbers' },
# { with: sum_equals_ten, message: 'must add up to 10' }
# ]
#
# FormatValidator.validate(user, 'email', options)
#
# @param model [Volt::Model] the model being validated
# @param field_name [String] the name of the field being validated
#
# @param options (see #apply)
# @option options (see #apply)
#
# @return (see #errors)
def self.validate(model, field_name, options)
new(model, field_name).apply(options).errors
end
# @param model [Volt::Model] the model being validated
# @param field_name [String] the name of the field being validated
def initialize(model, field_name)
@name = field_name
@value = model.get field_name
@criteria = []
end
# Applies criteria to the validator in a variety of forms
#
# @see .validate param examples
# @param options [Hash, Array<Hash>] criteria and related error messages
#
# @option options [Regexp, Proc] :with criterion for validation
# @option options [String] :message to display if criterion not met
# - will be appended to the field name
# - should start with something like:
# - +"must include..."+
# - +"should end with..."+
# - +"is invalid because..."+
#
# @return [self] returns itself for chaining
def apply(options)
return apply_list options if options.is_a? Array
options = case options
when true
default_options
when Hash
if default_options.is_a? Hash
default_options.merge options
else
options
end
end
with options[:with], options[:message]
self
end
# Returns the first of the validation errors or an empty hash
#
# @return [Hash] hash of validation errors for the field
# - +{}+ if there are no errors
# - +{ field_name: [messages] }+ if there are errors
def errors
valid? ? {} : { @name => error_messages }
end
# Returns an array of validation error messages
# @return [Array<String>]
def error_messages
@criteria.reduce([]) do |e, c|
test(c[:criterion]) ? e : e << c[:message]
end
end
# Returns true or false depending on if the model passes all its criteria
# @return [Boolean]
def valid?
error_messages.empty?
end
# Adds a criterion and error message
#
# @param criterion [Regexp, Proc] criterion for validation
# @param message [String] to display if criterion not met
# - will be appended to the field name
# - should start with something like:
# - +"must include..."+
# - +"should end with..."+
# - +"is invalid because..."+
#
# @return (see #apply)
def with(criterion, message)
@criteria << { criterion: criterion, message: message }
self
end
private
def apply_list(array)
array.each { |options| apply options }
self
end
def default_options
{}
end
def test(criterion)
return false unless @value.respond_to? :match
!!(criterion.try(:call, @value) || criterion.try(:match, @value))
end
end
end