[Dry::Struct] How to override a reader?

I’m trying to override a reader method on a Dry::Struct. I’ve got three approaches that work, but I’m not crazy about any of them. I was wondering if there’s an accepted best practice for doing this or if there’s just a better way to do this that I haven’t considered.

Here’s an example

class Thing < Dry::Struct 
  attribute :id, Types::String.enum("00", "02", "99")
end

I want the return value of Thing#name to always be 10 characters long and right-padded with 0s.

Approach #1

class Thing < Dry::Struct
  attribute :id, Types::String.enum("00", "02", "99")

  def id
    @id.ljust(10, "0")
  end
end

I don’t like this approach because it requires accessing the @id instance variable directly, which I consider to be an implementation detail of Dry::Struct and not part of its public interface.

Approach #2

class Thing < Dry::Struct
  attribute :id, Types::String.enum("00", "02", "99")

  alias orig_id id
  def id
    orig_id.ljust(10, "0")
  end
end

I think this approach is better than #1 but it’s kind of ugly, IMO.

Approach #3

module ThingOverrides
  def id
    super.ljust(10, "0")
  end
end
class Thing < Dry::Struct 
  prepend ThingOverrides
  attribute :id, Types::String.enum("00", "02", "99")
end

This approach is kind of elegant, but introducing a new module every time you want to override a method seems like overkill.

Any thoughts?

TBF I don’t see a problem in accessing instance variables from within an object. In addition, dry-struct doesn’t do anything beyond assigning ivars on the instance level so you’re not going to get in trouble here. One issue you may come across is initializing a new struct using Struct#new (i.e. instance level method) which in the current version (0.3.1) calls accessor methods rather than accessing ivars directly. This leads to a problem of accumulating changes added by accessors, see

class Foo < Dry::Struct
  attribute :price, Types::Strict::Int 
  attribute :foo, Types::Strict::String

  def price
    @price * 100
  end 
end 

Foo.new(price: 1, foo: 'foo').price # => 100
Foo.new(price: 1).new(foo: 'bar').price # => 10000

Obviously the last part should be

Foo.new(price: 1).new(foo: 'bar').price # => 100

It was fixed in master and we’ll push a release in some near future (a week or so I hope).

Thanks for the guidance, I’ll stick with approach #1.