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!