How to perform checks based on the result of the previous check


#1

I would like to assert that a value is a string is not blank (eg. " "). If I submit a number, I want only the message “must be a string” to appear, however, the “cannot be blank” message also appears. I have tried to debug this, and it seem that the not_blank? rule is failed automatically if the str? rule fails, as my debugger did not break when put in the not_blank? method. Is there a way to short circuit the checking, so that not_blank? is only checked if str? is true? I’m sure this has been answered before, however, I’ve had a bit of a search and cannot find an answer, and I’m not sure of the terms to search on. Thanks in advance.

require 'dry-validation' #v0.11.0

form = Dry::Validation.Form do
  configure do
    def not_blank?(value)
      !blank?(value)
    end

    def blank?(value)
      value.is_a?(String) && value.strip.empty?
    end

    def self.messages
      super.merge(
        en: { errors: { not_blank?: 'cannot be blank' } }
      )
    end
  end

  required(:name_one) { str? & not_blank? }
  required(:name_two).filled(:str?, :not_blank?)
  required(:name_three) { str? & (str? > not_blank?) }
end

puts form.call(name_one: 1, name_two: 2, name_three: 3).messages
# => {:name_one=>["must be a string", "cannot be blank"], :name_two=>["must be a string", "cannot be blank"], :name_three=>["must be a string", "cannot be blank"]}

#2

In fact, the checks are short-circuit, and if one check fails the rest are skipped. To understand why you are getting extra messages you need to understand what kinds of error messages dry-validation has. It is described here, but I will give you a summary:

  • errors: messages which are the result of failing predicates;
  • hints: messages for predicates which weren’t evaluated because an earlier predicate had failed;
  • messages: errors and hints combined.

So what you need is to use Dry::Validation::Result#errors instead of Dry::Validation::Result#messages as you don’t need hints.

Here’s an example:

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  gem 'dry-validation', '~> 0.11.1'
  gem 'rspec', require: 'rspec/autorun'
end

module BlankChecker
  def self.call(value)
    !value.to_s.strip.empty?
  end
end

Form = Dry::Validation.Form do
  configure do
    def not_blank?(value)
      # Delegate the check to a helper object because
      # frozen form instances cannot be mocked.
      BlankChecker.call(value)
    end

    def self.messages
      super.merge(
        en: { errors: { not_blank?: 'cannot be blank' } }
      )
    end
  end

  required(:name).filled(:str?, :not_blank?)
end

RSpec.describe 'Dry::Validation error messages' do
  subject(:result) { Form.(name: name) }

  context 'when input is a valid string' do
    let(:name) { 'George' }

    it "don't have error messages" do
      # Blank check is performed:
      expect(BlankChecker).to receive(:call).and_return(true)

      expect(result.messages).to be_empty
      expect(result.errors).to be_empty
      expect(result.hints).to be_empty
    end
  end

  context 'when input is a blank string' do
    let(:name) { '   ' }

    it 'have an error and no hints' do
      # Blank check is performed:
      expect(BlankChecker).to receive(:call).and_return(false)

      expect(result.messages).to eq(name: ['cannot be blank'])
      expect(result.errors).to eq(result.messages)
      expect(result.hints[:name]).to be_empty
    end
  end

  context 'when input is not a string' do
    let(:name) { 0xDEADBABE }

    it 'have an error and a hint' do
      # Blank check is NOT performed!
      expect(BlankChecker).not_to receive(:call)

      expect(result.messages[:name]).to eq(result.errors[:name] + result.hints[:name])
      expect(result.errors[:name]).to eq(['must be a string'])
      expect(result.hints[:name]).to eq(['cannot be blank'])
    end
  end
end

#3

That’s awesome, thank you. I had not seen the documentation for that feature.