amatsuda/active_decorator

Write active decorator DRY

Closed this issue · 5 comments

On writing my UserDecorator, I find it overlap a lot. Just decorate the datetime but I have to repeat writing decorator a lot.

# frozen_string_literal: true

module UserDecorator
  def created_at_datetime
    created_at&.strftime '%Y/%m/%m %H:%M:%S'
  end

  def confirmed_at_datetime
    confirmed_at&.strftime '%Y/%m/%m %H:%M:%S'
  end

  def locked_at_datetime
    locked_at&.strftime '%Y/%m/%m %H:%M:%S'
  end

  def current_sign_in_at_datetime
    current_sign_in_at&.strftime '%Y/%m/%m %H:%M:%S'
  end

  def last_sign_in_at_datetime
    last_sign_in_at&.strftime '%Y/%m/%m %H:%M:%S'
  end
end

And guess what, on my AdminDecorator I have the exact same fields. Do I have to copy all of these again to AdminDecorator?
Any advice guys?

I could use some meta code like these but I can't solve the exact same fields at AdminDecorator:

module UserDecorator
  DATETIME_DECORATOR_FIELDS = %w[created_at confirmed_at locked_at current_sign_in last_sign_in_at].freeze

  DATETIME_DECORATOR_FIELDS.each do |field|
    define_method("#{field}_datetime") do |format = nil|
      return send(field)&.strftime(format) if format

      send(field)&.strftime '%Y/%m/%m %H:%M:%S'
    end
  end
end

@truongnmt Decorators are basically just plain Ruby modules. And active_decorator itself doesn't provide any special utility for extending the application code.
So, your question is actually a general Ruby/Rails question that should not be posted here.

Anyway, as you answered yourself, using basic Ruby metaprogramming technique would effectively reduce your code.

Another solution I could come up with is to extend Active Record (Active Model) attribute API.
In this case, simply overriding _read_attribute in your decorator like the following would do the magic (but please don't forget to leave a detailed comment for your teammates!).

  def _read_attribute(attr_name, &block)
    if @attributes[attr_name].type.type == :datetime
      super&.strftime '%Y/%m/%m %H:%M:%S'
    else
      super
    end
  end

Nice idea! Thanks a lot!
By the way, I just ask at SO, so I gonna share here:
https://stackoverflow.com/questions/56844902/write-active-decorator-dry

@amatsuda Sorry for bring this issue back but I have a question.

I'm writing method_missing for above problem, basically I write in DatetimeDecorator and another decorator just extend that file:

module DatetimeDecorator
  DEFAULT_FORMAT = '%Y/%m/%d %H:%M:%S'

  def method_missing(method, *args, &block)
    attribute = method.to_s.sub(/_datetime\z/, '')
    super unless respond_to?(attribute)
    public_send(attribute).strftime(args[0] || DEFAULT_FORMAT)
  end

  def respond_to_missing?(method, include_private = false)
    attribute = method.to_s.sub(/_datetime\z/, '')
    respond_to?(attribute) || super
  end
end
module UserDecorator
  extend DatetimeDecorator
end

module AdminDecorator
  extend DatetimeDecorator
end

Usage is: user.created_at_datetime
However on calling above, it's not run into my method_missing, instead it jump into ActiveDecorator::Helpers#method_missing 🤔

Oh actually not extend, include is the correct way to do. However I don't know why yet @@