Custom predicated in Dry::Struct

Hi,

I am working on a tool to create dry struct from an xsd. The xsd has some constraints I would like to mimic. This isn’t it but I simplified the version. Ideally I wanted to add some options to constrained. I looked at the source to constrained source https://github.com/dry-rb/dry-types/blob/2ac0ba485a9c141377151e166861e0e418983495/lib/dry/types/builder.rb#L75-L77 which calls to Rule, which looks up the predicates from Dry::Logic::Predicates https://github.com/dry-rb/dry-types/blob/2ac0ba485a9c141377151e166861e0e418983495/lib/dry/types/constraints.rb#L13-L21. I wouldn’t like to append to that one but I could.

I got a solution working where I create new predicates but use them from a new constructor instead of using constrained. This work but doesn’t give the best errors when multiple constraints fails.

module Types
  include Dry::Types()
  extend Dry::Logic::Builder

  build do
    predicate :divisible_with? do |num, input|
      (input % num).zero?
    end
  end

  divisible_with_ten = build do
    divisible_with?(10)
  end

  LessThenthousend = Integer.constrained(lt: 1000)
  LessThenthousendDivisebleByTen = Types.Constructor(LessThenthousend) { |value, type|
    type.call(value)
      .tap {
        next if divisible_with_ten.call(_1).success?
        raise Dry::Types::ConstraintError.new("divisible_with_ten?", value)
      }
   }
end

# pp Types::LessThenthousend.call(12011)
# :in `call_unsafe': 12000 violates constraints (lt?(1000, 12000) failed) (Dry::Types::ConstraintError)

# pp Types::LessThenthousend.call(120)
# pp Types::LessThenthousendDivisebleByTen.call(121)
# pp Types::LessThenthousendDivisebleByTen.call(12100)

It this how it’s suppose to be done? any suggestions

I did play around more and crafted this. It works a lot better. Maybe this is something that should could be included in dry-types.

require "dry-struct"
require 'dry/logic'
require 'dry/logic/predicates'

module Dry
  module Types
    module MokeyPatchForCustomConstains
      def self.constrains(options)
        case options&.partition { |_,value| value.is_a?(Dry::Logic::Rule::Predicate) }
        in [[hack, *] => hacks, []]
          hack_predicates = hacks.map(&:second)
          hack_predicates.inject{ _1 & _2 }
        in [[hack, *] => hacks, std]
          hack_predicates = hacks.map(&:second)
          Types.Rule(std) & hack_predicates.inject{ _1 & _2 }
        in [[], std]
          Types.Rule(std)
        end
      end
    end

    module Builder
      def constrained(options)
        constrained_type
          .new(self, rule: MokeyPatchForCustomConstains::constrains(options))
      end
    end

    class Constrained
      def constrained(options)
        with(rule: rule & MokeyPatchForCustomConstains::constrains(options))
      end
    end
  end
end


module Types
  module MyPredicate
    module Methods
      def [](name)
        method(name)
      end

      def build(name, arg)
        ::Dry::Logic::Rule::Predicate
          .build(self[name])
          .curry(arg)
      end

      def between_these_two(num, input)
        lowest, higest = num
        input > lowest && input < higest
      end
    end

    extend Methods
  end

  is_between_3_and_7 = MyPredicate.build(:between_these_two, [3,7])
  is_between_5_and_16 = MyPredicate.build(:between_these_two, [5,16])

  include Dry::Types(:coercible, default: :coercible)

  Desimaltall = Decimal.constrained(lt: 10)

  # BeloepV4 = Desimaltall.constrained(gt: 4, lt: 8, is_between_3_and_7:, is_between_5_and_16:)
  # BeloepV4 = Decimal.constrained(is_between_3_and_7:, is_between_5_and_16:)

  BeloepV4 = Decimal
    .constrained(is_between_3_and_7:)
    .constrained(is_between_5_and_16:)
end

pp Types::BeloepV4.call("14")
pp Types::BeloepV4.call("14")