121 lines
3.5 KiB
Ruby
121 lines
3.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'concurrent/map'
|
|
|
|
require 'dry/equalizer'
|
|
require 'dry/initializer'
|
|
require 'dry/schema/path'
|
|
|
|
require 'dry/validation/config'
|
|
require 'dry/validation/constants'
|
|
require 'dry/validation/rule'
|
|
require 'dry/validation/evaluator'
|
|
require 'dry/validation/messages/resolver'
|
|
require 'dry/validation/result'
|
|
require 'dry/validation/contract/class_interface'
|
|
|
|
module Dry
|
|
module Validation
|
|
# Contract objects apply rules to input
|
|
#
|
|
# A contract consists of a schema and rules. The schema is applied to the
|
|
# input before rules are applied, this way you can be sure that your rules
|
|
# won't be applied to values that didn't pass schema checks.
|
|
#
|
|
# It's up to you how exactly you're going to separate schema checks from
|
|
# your rules.
|
|
#
|
|
# @example
|
|
# class NewUserContract < Dry::Validation::Contract
|
|
# params do
|
|
# required(:email).filled(:string)
|
|
# required(:age).filled(:integer)
|
|
# optional(:login).maybe(:string, :filled?)
|
|
# optional(:password).maybe(:string, min_size?: 10)
|
|
# optional(:password_confirmation).maybe(:string)
|
|
# end
|
|
#
|
|
# rule(:password) do
|
|
# key.failure('is required') if values[:login] && !values[:password]
|
|
# end
|
|
#
|
|
# rule(:age) do
|
|
# key.failure('must be greater or equal 18') if values[:age] < 18
|
|
# end
|
|
# end
|
|
#
|
|
# new_user_contract = NewUserContract.new
|
|
# new_user_contract.call(email: 'jane@doe.org', age: 21)
|
|
#
|
|
# @api public
|
|
class Contract
|
|
include Dry::Equalizer(:schema, :rules, :messages, inspect: false)
|
|
|
|
extend Dry::Initializer
|
|
extend ClassInterface
|
|
|
|
config.messages.top_namespace = 'dry_validation'
|
|
config.messages.load_paths << Pathname(__FILE__).join('../../../../config/errors.yml').realpath
|
|
|
|
# @!attribute [r] config
|
|
# @return [Config]
|
|
# @api public
|
|
option :config, default: -> { self.class.config }
|
|
|
|
# @!attribute [r] locale
|
|
# @return [Symbol]
|
|
# @api public
|
|
option :locale, default: -> { :en }
|
|
|
|
# @!attribute [r] schema
|
|
# @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
|
|
# @api private
|
|
option :schema, default: -> { self.class.__schema__ }
|
|
|
|
# @!attribute [r] rules
|
|
# @return [Hash]
|
|
# @api private
|
|
option :rules, default: -> { self.class.rules }
|
|
|
|
# @!attribute [r] message_resolver
|
|
# @return [Messages::Resolver]
|
|
# @api private
|
|
option :message_resolver, default: -> { Messages::Resolver.new(self.class.messages, locale) }
|
|
|
|
# Apply contract to an input
|
|
#
|
|
# @return [Result]
|
|
#
|
|
# @api public
|
|
def call(input)
|
|
Result.new(schema.(input), Concurrent::Map.new, locale: locale) do |result|
|
|
rules.each do |rule|
|
|
next if rule.keys.any? { |key| error?(result, key) }
|
|
|
|
rule.(self, result).failures.each do |failure|
|
|
result.add_error(message_resolver[failure])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Return a nice string representation
|
|
#
|
|
# @return [String]
|
|
#
|
|
# @api public
|
|
def inspect
|
|
%(#<#{self.class} schema=#{schema.inspect} rules=#{rules.inspect}>)
|
|
end
|
|
|
|
private
|
|
|
|
# @api private
|
|
def error?(result, key)
|
|
path = Schema::Path[key]
|
|
result.error?(path) || path.map.with_index { |k, i| result.error?(path.keys[0..i-2]) }.any?
|
|
end
|
|
end
|
|
end
|
|
end
|