Reputation: 4555
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:
attribute :field...
would become attribute :field_wrapper, multiple: true, serialize: true, field: "field"
)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
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