Attributes

Sometimes you need to access all attributes assigned via params and options of the object constructor.

We support 2 methods: attributes and public_attributes for this goal. Both methods are wrapped into container accessible via .dry_types container:

require 'dry-initializer'

class User
  extend Dry::Initializer

  param  :name
  option :email,   optional: true
  option :telefon, optional: true, as: :phone
end

user = User.new "Andy", telefon: "71002003040"

User.dry_initializer.attributes(user)
# => { name: "Andy", phone: "71002003040" }

What the method does is extracts variables assigned to the object (and skips unassigned ones like the email above). It doesn’t matter whether you send it via params or option; we look at the result of the instantiation, not at the interface.

Method public_attributes works different. Let’s look at the following example to see the difference:

require 'dry-initializer'

class User
  extend Dry::Initializer

  param  :name
  option :telefon,  optional: true, as: :phone
  option :email,    optional: true
  option :token,    optional: true, reader: :private
  option :password, optional: true, reader: false
end

user = User.new "Andy", telefon: "71002003040", token: "foo", password: "bar"

User.dry_initializer.attributes(user)
# => { name: "Andy", phone: "71002003040", token: "foo", password: "bar" }

User.dry_initializer.public_attributes(user)
# => { name: "Andy", phone: "71002003040", email: nil }

Notice that public_attribute reads public reader methods, not variables. That’s why it skips both the private token, and the password whose reader hasn’t been defined.

Another difference concerns unassigned values. Because the reader user.email returns nil (its @email variable contains Dry::Initializer::UNDEFINED constant), the public_attributes adds this value to the hash using the method.

The third thing to mention is that you can reload the reader, and it is the reloaded method will be used by public_attributes:

require 'dry-initializer'

class User
  extend Dry::Initializer

  param  :name
  option :password, optional: true

  def password
    super.hash.to_s
  end
end

user = User.new "Joe", password: "foo"

User.dry_initializer.attributes(user)
# => { user: "Joe", password: "foo" }

User.dry_initializer.public_attributes(user)
# => { user: "Joe", password: "-1844874613000160009" }

This feature works for the “extend Dry::Initializer” syntax. But what about “include Dry::Initializer.define …” one? Now we don’t pollute class namespace with new methods, that’s why .dry_initializer is absent.

To access config you can use a hack. Under the hood we define private instance method #__dry_initializer_config__ which refers to the same container. So you can write:

require 'dry-initializer'

class User
  extend Dry::Initializer
  param :name
end

user = User.new "Joe"

user.send(:__dry_initializer_config__).attributes(user)
# => { user: "Joe" }

user.send(:__dry_initializer_config__).public_attributes(user)
# => { user: "Joe" }

This is a hack because the __dry_initializer_config__ is not a part of the gem’s public interface; there’s a possibility it can be changed or removed in the later releases.

We’ll try to be careful with it, and mark it as deprecated method in case of such a removal.