Dry Validation - Constructor Types and nil inputs

Hey all!

I was using dry-validation and dry-types and ran into some odd behavior that feels somewhat unexpected. The situation came from using a Constructor type for a class that was, itself, using a dry type inside the initalizer.

Basically, on calling the contract a CoercionError bubbles all the way up (instead of being nested in the errors as I’d expect). I’m guessing this has to do with how Constructor types are “constrained” in dry types, but my expectation is that filled on a Contract would first reject any nil input.

Here’s a reproduction script to showcase what I mean.

module Types
  include Dry.Types()
end

# Later

class Thing
  CleanString = Types::Strict::String.constructor(&:strip).constructor(&:downcase)
  def initialize(input_string)
    @thing = CleanString[input_string]
  end
end


module Types
  Thing = self.Constructor(::Thing)
end

class SomeContract < Dry::Validation::Contract
  json do
    required(:thing).filled(Types::Thing)
  end
end

SomeContract.new.call(thing: nil)
# => Dry::Types::CoercionError: undefined method 'strip' for nil:NilClass


# With "Thing" using .try - bad result :(
class Thing
  CleanString = Types::Strict::String.constructor(&:strip).constructor(&:downcase)
  def initialize(input_string)
    @thing = CleanString.try(input_string)
  end
end

# Input looks "valid"
SomeContract.new.call(thing: nil)
#<Dry::Validation::Result{:thing=>#<Thing:0x00007f93b6ae2698 @thing=#<Dry::Types::Result::Failure input=nil error=#<Dry::Types::CoercionError: undefined method `strip' for nil:NilClass>>>} errors={}>

# With "Thing" without using Dry::Types at all
class Thing
  def initialize(input_string)
    @thing = input_string.strip.downcase
  end
end

SomeContract.new.call(thing: nil)
# => #<Dry::Validation::Result{:thing=>nil} errors={:thing=>["must be Thing"]}>

– edit –
meant to also say that I did some prior reading here: Contract ignores ConstraintError thrown by Dry::Types[Constructor]

Which is close, but not exactly what I meant. I don’t expect some custom constraint message, at most I just expected that there wasn’t an outright error bubbled up.

dry-validation doesn’t catch exceptions, it would be too slow. It cannot “magically” construct a value for you either, your constructor must be safe to call. It doesn’t actually matter if it returns a Thing or not because later it’ll run predicated against the returned value. An example of a constructor following these rules:

  NotEmptyString = Types::String.constructor do |str, &block|
    if str.is_a?(::String)
      result = str.strip

      if result.empty?
        block.(nil)
      else
        result
      end
    else
      block.(str)
    end
  end

Using block.(...) doesn’t make much difference in this case.