Today I Learned

Dependent types in Ruby with dry-rb

Dry-rb supports dependent types (i.e., it allows you to match types based on the value):

class HasA < Dry::Struct
  attribute :a, Types::Symbol
end

class HasB < Dry::Struct
  attribute :b, Types::Symbol
end

class ShortText < Dry::Struct
  attribute :text, Types::String.constrained(max_size: 3)
end

class LongText < Dry::Struct
  attribute :text, Types::String.constrained(min_size: 4)
end

class OurStruct < Dry::Struct
  attribute :a_or_b, Types::Array.of(A | B)
  attribute :texts, Types::Array.of(ShortText | LongText)
end

> struct = OurStruct.new(
>   a_or_b: [{ a: :abc } , { b: :def }],
>   texts: [{ text: 'abc' } , { text: 'defgh' }],
> )
=> #<OurStruct
=> #  a_or_b=[#<A a=:abc>, #<B b=:def>]
=> #  texts=[#<ShortText text="abc">, #<LongText text="defgh">]
=> #>

You can use it to, e.g., parse data and then attach specific behavior to it:

module Types
  EventData = Types::Hash.schema(name: Types::String, value: Types::Integer)
end

class GroupCondition < Dry::Struct
  attribute :event, Types::Value(:group_formed)
  attribute :data, Types::EventData

  def applicable?
    Group.exists?(name: data.fetch(:name)) && data.fetch(:value) >= 123
  end
end

class InvitationCondition < Dry::Struct
  attribute :event, Types::Value(:invitation_sent)
  attribute :data, Types::EventData

  def applicable?
    Invitation.exists?(value: data.fetch(:value))
  end
end

class ConditionsSet < Dry::Struct
  attribute :conditions, Types::Array.of(GroupCondition | InvitationCondition)
end

> set = ConditionsSet.new(
>   conditions: [
>     { event: :invitation_sent, data: { name: 'abc', value: 123 } },
>     { event: :group_formed, data: { name: 'def', value: 456 } }
>   ]
> )
=> #<ConditionsSet
=> #  conditions=[
=> #    <InvitationCondition event=:invitation_sent data={:name=>"abc", :value=>123}>,
=> #    <GroupCondition event=:group_formed data={:name=>"def", :value=>456}>
=> #  ]
=> #>
> set.conditions.all?(&:applicable?)
=> true