How to handle a dry struct with array attribute with multiple members type


#1

Hi guys! Hope you’re all well.

Playing with dry-struct and dry-types, and wanted to add an array with members of two types, where both types have same attribute BUT different type definition (one string, another a hash struct). Example below:

require "dry-struct"
require "dry-types"

module Types
  include Dry::Types.module
end

class Answer < Dry::Struct
  attribute :question_id, Types::String

  class Select < self
    attribute :value, Types::Hash.schema(id: Types::Int)
  end

  class Text < self
    attribute :value, Types::String
  end
end

class Survey < Dry::Struct
  attribute :answers, Types::Array.member(Answer::Text | Answer::Select)
end

Survey.new(
  answers: [
    Answer::Select.new(question_id: 1, value: {id: 10}),
    Answer::Text.new(question_id: 1, value: "foo")
  ]
)

#<Survey answers=[#<Answer::Text question_id=1 value={:id=>10}>, #<Answer::Text question_id=1 value="foo">]>

Problem here is, both answers came back as Answer::Text.

If I invert the order of the members to .member(Answer::Select | Answer::Text), things blow up.

The other thing I tried was to have .member(Answer) since Select and Text are subclasses of Answer. It doesn’t blow up but turns Answer::Select into an Answer, getting rid of attribute :value.

How can I achieve members of an array to accept subclasses of a type?
Appreciate if someone can shed some light for me!

Thanks heaps! :slight_smile:


#2

This is gonna work:

require "dry-struct"
require "dry-types"
require 'byebug'

module Types
  include Dry::Types.module
end

class Answer < Dry::Struct
  attribute :question_id, Types::String

  class Select < self
    attribute :value, Types::Strict::Hash.schema(id: Types::Int)
  end

  class Text < self
    attribute :value, Types::Strict::String
  end
end

class Survey < Dry::Struct
  attribute :answers, Types::Array.member(Answer::Text | Answer::Select)
end

 Survey.new(
  answers: [
    Answer::Select.new(question_id: 1, value: {id: 10}),
    Answer::Text.new(question_id: 1, value: "foo")
  ]
)

Notice that I changed value definitions to use strict types, otherwise there’s no way to validate which input is valid for a specific type, hence it didn’t work.


#3

Sweeet! That is great!

That brings me another question I haven’t found the answer on dry-rb.org. When should I use Types::String as opposed to Types::Strict::String?

From my irb session, the Strict one returns a constrained, but should Type::String fail by default if that isn’t a string?

Thanks heaps for your help and really appreciate the effort you have put into the dry libraries.

Cheers
Ricardo


#4

Types::String is a plain definition that is typically used for annotation purposes. ie in rom-rb it used for relation schemas, where we define attribute types with various meta-data. You should use strict types in places where you want to enforce constraints and you’d rather see an exception than an invalid value being silently passed around in your system.