Type Constraints

Base Syntax

Use :type key in a param or option declarations to add type coercer.

require 'dry-initializer'

class User
  extend Dry::Initializer
  param :name, type: proc(&:to_s)
end

user = User.new :Andrew
user.name # => "Andrew"

Any object that responds to #call with 1 argument can be used as a type. Common examples are proc(&:to_s) for strings, method(:Array) (for arrays) or Array.method(:wrap) in Rails, ->(v) { !!v } (for booleans), etc.

Dry Types as coercers

Another important example is the usage of dry-types as type constraints:

require 'dry-initializer'
require 'dry-types'

class User
  extend Dry::Initializer
  param :name, type: Dry::Types['strict.string']
end

user = User.new :Andrew # => #<TypeError ...>

Positional Argument

Instead of :type option you can send a constraint/coercer as the second argument:

require 'dry-initializer'
require 'dry-types'

class User
  extend Dry::Initializer
  param :name,  Dry::Types['coercible.string']
  param :email, proc(&:to_s)
end

Array Types

As mentioned above, the :type option takes a callable object... with one important exception.

You can use arrays for values that should be wrapped to array:

class User
  extend Dry::Initializer

  option :name,   proc(&:to_s)
  option :emails, [proc(&:to_s)]
end

user = User.new name: "joe", emails: :"joe@example.com"
user.emails # => ["joe@example.com"]

user = User.new name: "jane", emails: [:"jane@example.com", :"jane@example.org"]
user.emails # => ["jane@example.com", "jane@example.org"]

You can wrap the coercer into several arrays as well:

class User
  extend Dry::Initializer

  option :emails, [[proc(&:to_s)]]
end

user = User.new name: "joe", emails: "joe@example.com"
user.emails # => [["joe@example.com"]]

Eventually, you can use an empty array as a coercer. In that case we just wrap the source value(s) into array, not modifying the items:

class Article
  extend Dry::Initializer

  option :tags, []
end

article = Article.new(tags: 1)
article.tags # => [1]

Nested Options

Sometimes you need to describe a structure with nested options. In this case you can use a block with options inside.

class User
  extend Dry::Initializer

  option :name, proc(&:to_s)

  option :emails, [] do
    option :address,     proc(&:to_s)
    option :description, proc(&:to_s)
  end
end

user = User.new name: "joe",
                emails: { address: "joe@example.com", description: "Job email" }

user.emails.class         # => Array
user.emails.first.class   # => User::Emails
user.emails.first.address # => "joe@example.com"

user.emails.to_h # => [{ address: "joe@example.com", description: "Job email" }]

Notice how we mixed array wrapper with a nested type.

The only syntax restriction here is that you cannot use a positional param inside the block.

Back References

Sometimes you need to refer back to the initialized instance. In this case use a second argument to explicitly give the instance to a coercer:

class Location < String
  attr_reader :parameter # refers back to its parameter

  def initialize(name, parameter)
    super(name)
    @parameter = parameter
  end
end

class Parameter
  extend Dry::Initializer
  param :name
  param :location, ->(value, param) { Location.new(value, param) }
end

offset = Parameter.new "offset", location: "query"
offset.name     # => "offset"
offset.location # => "query"
offset.location.parameter == offset # true