Constraint errors ignored with dry-validation type_specs

#1

Issue
I have a Dry::Struct and a custom Constructor which checks some constraints before creating an instance of that struct. For my internal code, I want to make sure I hit errors during test if I try and use that constructor with invalid arguments. This works as expected.

I would like to use that same constructor for preprocessing for my Validation.Params. However, I was surprised to find that if my custom constructor raises a constraint error, the validator succeeds and passes the value through in its raw form.

One way to ensure validation fails is to use the :type? predicate on the resulting value. However, the error message for this is not very helpful as it does not include the actual constraint that failed.

Any suggestions on how to make my validator fail with a more useful error? I would like to avoid duplicating the constraint definition/message, and I would also like to avoid having to post-process the form value.

Example Code
In the example code, I want to make sure a SmallHorse can only be created with a name that ends in “y”. I would like my form to fail with a message that indicates this constraint.

##
# Type & Schema definition
require 'dry-types'
require 'dry-struct'
require 'dry-validation'
require 'dry-logic'

module Types
  include Dry::Types.module

  class Horse < Dry::Struct
    attribute :name, Types::Strict::String
    attribute :size, Types::Strict::String.enum('small', 'medium', 'large')
  end

  SmallHorse = Constructor(Horse) do |name|
    ends_in_y = Types::Strict::String.constrained(format: /y$/)
    constrained_name = ends_in_y[name]
    Horse.new(name: constrained_name, size: 'small')
  end
end

SmallHorseForm = Dry::Validation.Params do
  configure { config.type_specs = true }

  # Option 1: Does not raise error when constructor fails
  # required(:horse, Types::SmallHorse)

  # Option 2: Resulting error message: "must be Types::Horse"
  required(:horse, Types::SmallHorse).value(type?: Types::Horse)
end

##
# Tests I want to pass
require 'rspec'

RSpec.describe Types::SmallHorse do
  context 'when the horse param ends in "y"' do
    it 'creates a small horse' do
      horse = Types::SmallHorse['Dippy']

      expect(horse).to be_a_kind_of(Types::Horse)
      expect(horse.name).to eq 'Dippy'
      expect(horse.size).to eq 'small'
    end
  end

  context 'when the horse param ends in "o"' do
    it 'raises a constraint error' do
      expect { Types::SmallHorse['Lungo'] }
        .to raise_error(Dry::Types::ConstraintError)
    end
  end

  context 'when the horse param is a number' do
    it 'raises a constraint error' do
      expect { Types::SmallHorse[99] }
        .to raise_error(Dry::Types::ConstraintError)
    end
  end
end

RSpec.describe 'SmallHorseForm' do
  context 'when the horse param ends in "y"' do
    it 'succeeds' do
      result = SmallHorseForm.call(horse: 'Dippy')

      expect(result).to be_success
      expect(result[:horse]).to be_a_kind_of(Types::Horse)
      expect(result[:horse].name).to eq 'Dippy'
    end
  end

  context 'when the horse param ends in "o"' do
    it 'fails' do
      result = SmallHorseForm.call(horse: 'Lungo')

      expect(result).to be_failure
      expect(result.messages).to include(:horse)
      expect(result.messages[:horse].first).to match /name must end in "y"/
    end
  end
    indent preformatted text by 4 spaces
  context 'when the horse param is a number' do
    it 'fails' do
      result = SmallHorseForm.call(horse: 99)

      expect(result).to be_failure
      expect(result.messages).to include(:horse)
      expect(result.messages[:horse].first).to match /name must end in "y"/
    end
  end
end
1 Like
#2

Happens to me as well. Wonder that this basic feature is not supported well

#3

This was improved in master and will be available in 1.0.0. In 0.x this feature was always experimental and it got properly done for 1.0.0 (beta is available on rubygems).