How to create custom types with validation, contraints and/or coercion?


#1

I have been digging trying to find a way to define my own types (custom coercion and validity checks and constraints). For example I want a type that checks that a given value is_a? MyClass. I found a way to do coercions from the base types here https://discuss.dry-rb.org/t/coercing-to-domain-model/69/2 but I do not know how to express custom constraints or build something that isn’t based on existing types.

To put it more concrete, how do I create a type that ensures that the given value is an Either (an instance of either Right or Left)? That way I can create for instance a struct and define something like attribute :result, Types::Either.

I tried my best looking in the code to find a way but the types repo is quite complex and I couldn’t figure it out on my own so any help is appreciated. Thanks!


#2

Why do you need to check if it’s an instance of Either? It doesn’t feel like part of data validation process. Could you provide some more context?

ps. sorry for late reply, I was on holidays and then catching up with work etc. so I’m slowly dealing with my email backlog


#3

We use dry-types as “steps” in between business logic chunks to verify what comes out of a step for debugging purposes; we also put them at the begging/end of a method or callable object so that you know can follow the ‘types’ of the code that you are reading without executing it.

With dry-types we ensure that contract breaches don’t expand to larger bugs in the system which has occurred a lot to us in distributed services. So, for instance if an Operations::CreateUser exists we want to know for sure that it returns an Either or an exceptions is raised if the contract’s breached. Sometimes we can check with is_a? but if it’s more complex than that we want to use all the goodies in ‘dry-types’ hence the need to create more custom types that would still be composable and have maybe and | for enums, etc.

It’s indeed not about the data validation as with dry-validation (hence the dry-types tag) but it’s within the context of ‘types’ to ensure expectations of returning object of pieces of code are fulfilled. This also helps to make sure that stubbed things in a container of dependencies doesn’t return something that if different in production, would result in an exception.

        find_transaction.call(txnum).or {
          # something happens here that should return a transaction at all times
        }.fmap(Types::Transaction) # Verify the returned type
      end
    end

Hope this clarified the need. Is this possible as of today?


#4

You can use constrained types with all kinds checks applied, question is - how do you want to handle failures?

To define a constrained type that checks if a value is an Either just do this:

irb(main):005:0> StrictEither = Dry::Types::Definition.new(Dry::Monads::Either).constrained(type: Dry::
=> #<Dry::Types::Constrained ...>
irb(main):006:0> StrictEither[nil]
Dry::Types::ConstraintError: nil violates constraints (type?(Dry::Monads::Either, nil) failed)
irb(main):007:0> StrictEither[Dry::Monads.Left('oops')]
=> Left("oops")
irb(main):008:0> StrictEither[Dry::Monads.Right('ok')]
=> Right("ok")