dry-validation/lib/dry/validation/rule.rb

136 lines
3.2 KiB
Ruby

# frozen_string_literal: true
require "dry/equalizer"
require "dry/validation/constants"
require "dry/validation/function"
module Dry
module Validation
# Rules capture configuration and evaluator blocks
#
# When a rule is applied, it creates an `Evaluator` using schema result and its
# block will be evaluated in the context of the evaluator.
#
# @see Contract#rule
#
# @api public
class Rule < Function
include Dry::Equalizer(:keys, :block, inspect: false)
# @!attribute [r] keys
# @return [Array<Symbol, String, Hash>]
# @api private
option :keys
# @!attribute [r] macros
# @return [Array<Symbol>]
# @api private
option :macros, default: proc { EMPTY_ARRAY.dup }
# Evaluate the rule within the provided context
#
# @param [Contract] contract
# @param [Result] result
#
# @api private
def call(contract, result)
Evaluator.new(
contract,
keys: keys,
macros: macros,
block_options: block_options,
result: result,
values: result.values,
_context: result.context,
&block
)
end
# Define which macros should be executed
#
# @see Contract#rule
# @return [Rule]
#
# @api public
def validate(*macros, &block)
@macros = parse_macros(*macros)
@block = block if block
self
end
# Define a validation function for each element of an array
#
# The function will be applied only if schema checks passed
# for a given array item.
#
# @example
# rule(:nums).each do
# key.failure("must be greater than 0") if value < 0
# end
# rule(:nums).each(min: 3)
# rule(address: :city) do
# key.failure("oops") if value != 'Munich'
# end
#
# @return [Rule]
#
# @api public
def each(*macros, &block)
root = keys[0]
macros = parse_macros(*macros)
@keys = []
@block = proc do
unless result.base_error?(root) || !values.key?(root)
values[root].each_with_index do |_, idx|
path = [*Schema::Path[root].to_a, idx]
next if result.error?(path)
evaluator = with(macros: macros, keys: [path], &block)
failures.concat(evaluator.failures)
end
end
end
@block_options = map_keywords(block) if block
self
end
# Return a nice string representation
#
# @return [String]
#
# @api public
def inspect
%(#<#{self.class} keys=#{keys.inspect}>)
end
# Parse function arguments into macros structure
#
# @return [Array]
#
# @api private
def parse_macros(*args)
args.each_with_object([]) do |spec, macros|
case spec
when Hash
add_macro_from_hash(macros, spec)
else
macros << Array(spec)
end
end
end
def add_macro_from_hash(macros, spec)
spec.each do |k, v|
macros << [k, v.is_a?(Array) ? v : [v]]
end
end
end
end
end