Nerdmaster
Nerdmaster

Reputation: 4555

How does one alias a method that does not yet exist? (Ruby Decorator)

This is going to sound pretty crazy, but I'm trying to build a generic decoration-based system which will allow a decorated class to do all kinds of crazy stuff with attributes. The goal is to be able to define attributes at a high level, decorate an ORM class (ActiveRecord, for example, though our primary case is actually quite a bit different), and use those decorations in various places in the app to automate some dynamic "magic' our app needs. For instance, we'll use the attributes to automatically generate forms and views, translate complex form hashes into flatter structures, etc.

To accommodate both use cases we've identified so far, I have a mixable module and a decorator (using Draper so Rails form magic still works, though I'm not married to Draper necessarily) which look more or less like this (obviously lots of details are omitted):

class DecoratorThing < Draper::Decorator
  include CoreMixinStuff
  delegate_all
end

module CoreMixinStuff
  extend ActiveSupport::Concern

  module ClassMethods
    def attribute(stuff, blah)
      attribute = AttributeDefinition.new(...)
      add_translation_methods(attribute)
      ...
    end

    def add_translation_methods(attribute)
      name = attribute.name
      reader = name
      writer = "#{name}="

      # In the case of field wrappers, we have to alias the original reader and writer so we
      # don't overwrite them completely
      if attribute.translation_type == :wrapper
        alias_method :"_orig_#{reader}", reader
        alias_method :"_orig_#{writer}", writer

      # Otherwise, we need to error if the reader or writer would collide
      elsif instance_methods.include?(reader) || instance_methods.include?(writer)
        raise RuntimeError.new("Cannot define an attribute which overrides existing methods (#{name.inspect})")
      end
    end
  end
end

Then the actual decorator for a specific instance does things like this:

class FooDecorator < DecoratorThing
  decorates Foo
  attribute :field, multiple: true, serialize: true
  attribute :field2, field: :delegation_field
  attribute :field3 do |field|
    field.subtype ...
    field.subtype ...
  end
end

The intent there would be to allow Foo#field to take an array and serialize it internally into a string before sending it off to wherever the decorated object takes it. Foo#field2 would just pass data as-is to delegation_field. Foo#field3 would take a complex hash of data and delegate it to the subtype fields.

The latter two cases are painful, but I have them working in a prototype. The first is the problem because of the alias_method stuff above - since the attribute method is run on the decorator, the method I'm trying to alias doesn't actually exist yet. It's not until FooDecorator.new(some_foo_instance) is called that those other instance methods are available.

I think my options are limited to the following, but I'm hoping there's some better choice:

The fourth option is probably the sanest, but I've made a lot of assumptions around being able to serialize, so if somebody else knows a nice way to make this happen, that'd be super swell.

Upvotes: 1

Views: 702

Answers (1)

br3nt
br3nt

Reputation: 9596

I think I have an answer that fits this question.

Basically, my solution adds a proxy column over the top of an existing column associated with an active-record model.

This proxy column overrides the original column's accessor methods. The basic proxy functionality doesn't do much beside set and get the value of original columns.

The actual code is quite long so I've created a gist for it. Check it out here.

The proxy column object can be subclassed/overridden to provide any sort of functionality required. For example, in your code AttributeDefinition would subclass ProxyColumn. The AttributeDefinition would act as a proxy between the different attributes and the values. It could wrap/unwrap the attributes value in a DecoratorThing based on whatever conditions that you deem appropriate.

One thing it doesn't do, at this stage, is provide a way of overriding how/what methods are added. However, all that could easily be changed based on your needs. I actually based that part off your add_translation_methods method anyways.

This is the basic usage to define the columns:

class MyModel

  # Add the concern to get the functionality
  include DataCollectionElements # concern

  # create proxy column and set various options
  proxy_column :my_col_1
  proxy_column :my_col_2, :alias => [:col1, col2, col3]
  proxy_column :my_col_3, :column => :actual_col_name

  # define the proxy object using class/string/symbol
  proxy_column :my_col_4, :proxy => MyProxy  # or 'MyProxy' or :my_proxy

  # define the proxy object using a proc/lambda
  proxy_column :my_col_5, :proxy => ->(model, code, options) { MyProxy.new(model, code, options) }
  proxy_column :my_col_6, :proxy => proc {|model, code, options| MyProxy.new(model, code, options) }

  # define the proxy object using a block
  proxy_column :my_col_7 do |model, code, options|
    MyProxy.new(model, code, options)
  end
end

Upvotes: 0

Related Questions