Pattern matching
Ruby 2.7 added experimental support for pattern matching. dry-validation supports pattern matching on result values starting with 1.4.1.
class PersonContract < Dry::Validation::Contract
  params do
    required(:first_name).filled(:string)
    required(:last_name).filled(:string)
  end
end
contract = PersonContract.new
case contract.('first_name' => 'John', 'last_name' => 'Doe')
in { first_name:, last_name: } => result if result.success?
  puts "Hello #{first_name} #{last_name}"
in _ => result
  puts "Invalid input: #{result.errors.to_h}"
end
Alternatively, results can be matched as a 2-value tuple of two hashes. The hash is validation output as in the previous example. The second is the context value shared between rules.
class AddressContract < Dry::Validation::Contract
  option :address_repo
  params do
    required(:address).filled(:string)
  end
  rule(:address) do |context:|
    address = address_repo.find(value)
    context[:address] = address if address
  end
end
contract = AddressContract.new(address_repo: AddressRepo.new)
case contract.('name' => 'John Doe', 'address' => 'Pedro Moreno 10, Ciudad de México')
in [{ name: }, { address: }] => result if result.success?
  # adding person to existing address
in { name:, address: } => result if result.success?
  # adding person to new address
else
  # showing errors
end
Pattern matching with monads
It may get tedious to write if result.success? every time. Another option is using the :monads extention that wraps Result objects with Success/Failure constructors.
require 'dry/validation'
require 'dry/monads'
Dry::Validation.load_extensions(:monads)
class CreatePerson
  include Dry::Monads[:result]
  class Contract < Dry::Validation::Contract
    params do
      required(:first_name).filled(:string)
      required(:last_name).filled(:string)
    end
  end
  attr_reader :repo
  def initialize(repo)
    @repo = repo
  end
  def call(input)
    case contract.(input).to_monad
    in Success(first_name:, last_name:)
      Success(repo.create(first_name, last_name))
    in Failure(result)
      Failure(result.errors.to_h)
    end
  end
  def contract
    @contract ||= Contract.new
  end
end
In this example it is important to have monads included in the class with include Dry::Monads[:result] because of how pattern matching works in Ruby. Still, it's neat!