class Dry::Validation::Contract

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

Extension to use dry-logic predicates as macros.

@see Dry::Validation::PredicateRegistry::WHITELIST Available predicates

@example

Dry::Validation.load_extensions(:predicates_as_macros)

class ApplicationContract < Dry::Validation::Contract
  import_predicates_as_macros
end

class AgeContract < ApplicationContract
  schema do
    required(:age).filled(:integer)
  end

  rule(:age).validate(gteq?: 18)
end

AgeContract.new.(age: 17).errors.first.text
# => 'must be greater than or equal to 18'

@api public

Public Class Methods

import_predicates_as_macros() click to toggle source

Make macros available for self and its descendants.

# File lib/dry/validation/extensions/predicates_as_macros.rb, line 61
def self.import_predicates_as_macros
  registry = PredicateRegistry.new

  PredicateRegistry::WHITELIST.each do |name|
    register_macro(name) do |macro:|
      predicate_args = [*macro.args, value]
      message_opts = registry.message_opts(name, predicate_args)

      key.failure(name, message_opts) unless registry.(name, predicate_args)
    end
  end
end

Public Instance Methods

call(input) click to toggle source

Apply the contract to an input

@param [Hash] input The input to validate

@return [Result]

@api public

# File lib/dry/validation/contract.rb, line 93
def call(input)
  Result.new(schema.(input), Concurrent::Map.new) do |result|
    rules.each do |rule|
      next if rule.keys.any? { |key| error?(result, key) }

      rule_result = rule.(self, result)

      rule_result.failures.each do |failure|
        result.add_error(message_resolver.(**failure))
      end
    end
  end
end
inspect() click to toggle source

Return a nice string representation

@return [String]

@api public

# File lib/dry/validation/contract.rb, line 112
def inspect
  %(#<#{self.class} schema=#{schema.inspect} rules=#{rules.inspect}>)
end

Private Instance Methods

error?(result, spec) click to toggle source

@api private

# File lib/dry/validation/contract.rb, line 119
def error?(result, spec)
  path = Schema::Path[spec]

  if path.multi_value?
    return path.expand.any? { |nested_path| error?(result, nested_path) }
  end

  return true if result.schema_error?(path)

  path
    .to_a[0..-2]
    .any? { |key|
      curr_path = Schema::Path[path.keys[0..path.keys.index(key)]]

      return false unless result.schema_error?(curr_path)

      result.errors.any? { |err|
        (other = Schema::Path[err.path]).same_root?(curr_path) && other == curr_path
      }
    }
end
macro(name, *args) click to toggle source

Get a registered macro

@return [Proc,#to_proc]

@api private

# File lib/dry/validation/contract.rb, line 146
def macro(name, *args)
  (macros.key?(name) ? macros[name] : Macros[name]).with(args)
end
messages() click to toggle source

Return configured messages backend

@return [Dry::Schema::Messages::YAML, Dry::Schema::Messages::I18n]

@api private

# File lib/dry/validation/contract.rb, line 155
def messages
  self.class.messages
end