Skip to content

Instantly share code, notes, and snippets.

@br3nt
Last active August 29, 2015 14:15
Show Gist options
  • Save br3nt/cd7f90991a9fa4b87eb5 to your computer and use it in GitHub Desktop.
Save br3nt/cd7f90991a9fa4b87eb5 to your computer and use it in GitHub Desktop.
ProxyColumns
class MetaDataProxyColumn < ProxyColumn
def initialize(model_class, code, options = {})
super(model_class, code, options)
end
# This method associates an object with our underlying column
# We can use this object to store/hold additional data/functionality for our column
def column_meta_data
@column_meta_data ||= ColumnMetaData.find_by_code(source_column.name)
raise "Unable to find DataCollectionElement with code `#{@code}`" unless @column_meta_data
@column_meta_data
end
# We're adding a little bit of logic here based on the column_meta_data.
#
# This method gets the value from the actual underlying column then
# defers to our metadata object if the column is a lookup.
#
def get_subject_value(model, alias_writer_method)
# get value from column
value = super(model, alias_writer_method)
# do some logic to return a value
if column_meta_data.is_lookup?
column_meta_data.some_method_that_returns_a_value(value)
else
value
end
end
def to_s
column_meta_data.try(:description) || ''
end
end
class ProxyColumn
attr_reader :model_class, :code, :source_column_name
def initialize(model_class, code, options = {})
@model_class = model_class
@code = code
@source_column_name = options[:column] || code
end
def source_column
@source_column ||= @model_class.columns.find {|col| col.name.to_sym == @source_column_name }
raise "Column not defined on #{model_class.name} for data collection element: #{code}" unless @source_column
@source_column
end
def get_subject_value(model, alias_writer_method)
model.read_attribute(:alias_writer_method)
end
def set_subject_value(model, alias_reader_method, value)
model.write_attribute(:alias_reader_method, value)
end
def delegate_to_subject(model, method_name, *value)
args = [method_name] + value
model.send(*args)
end
end
module ProxyColumns
extend ActiveSupport::Concern
module ClassMethods
# Registers a ProxyColumn with an ActiveRecord model
#
# Options:
# * :column - specify the column that holds the value for this element
# * :alias - create aliases for the reader/writer methods
# * :proxy - Used to create a specific type of proxy object
# Takes any of the following:
# - Name of a class as string, symbol (snake_case or Camelized)
# - Class
# - Proc/Lambda that returns one of the above
# The resulting proxy object should respond to:
# - get_subject_value(model, alias_writer_method)
# - set_subject_value(model, alias_reader_method, value)
# - delegate_to_subject(model, method_name, *value) (optional)
#
# If a block is given, the proxy object will be the value returned by the block.
#
def proxy_column(code, options = {}, &block)
code = code.to_sym
# determine the class of the proxy object
# see: http://blog.rubybestpractices.com/posts/gregory/anonymous_class_hacks.html
proxy = options.delete(:proxy) || ProxyColumn
proxy =
case proxy
when String, Symbol ; proxy.to_s.camelize.constantize
when Class ; proxy
when Proc ; proxy.call
end
proxy = Class.new(proxy, &block)
proxy = proxy.new(code, options)
# get all aliases
aliases = [*options[:alias]].each(&:to_sym)
code_and_aliases = [code] + aliases
# queue proxy to have its reader/writer methods defined
code_and_aliases.each do |code_and_alias|
pending_proxy_columns[code] = proxy
end
proxy_columns[code] = proxy
proxy_column_aliases[code] = aliases
end
# The ProxyColumns registered with this ActiveRecord model
def proxy_columns
@proxy_columns ||= HashWithIndifferentAccess.new
end
# The ProxyColumns registered with this ActiveRecord model
def proxy_column_aliases
@proxy_column_aliases ||= HashWithIndifferentAccess.new
end
end
# Defines the accessor/reader methods for the ProxyColumn
def alias_proxy_columns(code, proxy, aliases)
# alias our attribute
attribute = proxy.source_column_name
alias_attribute code, attribute unless code.to_s == attribute.to_s
# alias_attribute generates a plethora of methods that can be overridden
# these methods can be determined by calling ActiveRecord::Base.attribute_method_matchers
attribute_reader = :"#{code}"
attribute_writer = :"#{code}="
# attribute_before_type_cast = :"#{code}_before_type_cast"
# attribute_flag = :"#{code}?"
# attribute_changed_flag = :"#{code}_changed?"
# attribute_change = :"#{code}_change"
# attribute_will_change_bang = :"#{code}_will_change!"
# attribute_was = :"#{code}_was"
# reset_attribute_bang = :"reset_#{code}!"
# override the reader method
define_method attribute_reader { column.get_subject_value(self, alias_reader) }
# define the writer method
define_method attribute_writer {|value| column.set_subject_value(self, alias_writer, value) }
# NOTE: can define all sorts of methods that delegate to proxy
define_method attribute_before_type_cast { column.delegate_to_subject(self, attribute_before_type_cast) }
define_method attribute_flag { column.delegate_to_subject(self, attribute_flag) }
define_method attribute_changed_flag { column.delegate_to_subject(self, attribute_changed_flag) }
define_method attribute_change { column.delegate_to_subject(self, attribute_change) }
define_method attribute_will_change_bang { column.delegate_to_subject(self, attribute_will_change_bang) }
define_method attribute_was { column.delegate_to_subject(self, attribute_was) }
define_method reset_attribute_bang { column.delegate_to_subject(self, reset_attribute_bang) }
# define additional aliases
aliases.each do |attribute_alias|
alias_attribute attribute_alias, code
proxy_column_aliases[code] ||= []
proxy_column_aliases[code] << attribute_alias
end
end
end

ProxyColumns

ProxyColumns 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 Proxy object can be subclassed/overridden to provide any sort of functionality required. This enables additional functionality to be added on top of the underlying column. A ProxyColumn can delegate based on the column and the column's value, as seen in MetaDataProxyColumn.

One of the problems I faced was that the accessor methods for a column on a model weren't generated until the first model was initialised. That is why the methods are added dynamically using the after_initialize callback.

Include the concern:

ActiveRecord::Base.include(ProxyColumns) # concern

Different ways of instantiating:

    class MyModel
     
      # Alternativly, add the concern on a per model basis
      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 => -> { MyProxy.new(model, code, options) }
      proxy_column :my_col_6, :proxy => proc { MyProxy.new(model, code, options) }
    
      # define the proxy object using a block
      proxy_column :my_col_7 do |code, options|
        def some_custom_method
          'some return value'
        end
      end

      # redefine the proxy object using a block and a specified base class
      proxy_column :my_col_7, :proxy => MyProxy do |code, options|
        def some_custom_method
          'some return value'
        end
      end
    end

Feature wishlist

  • Define a method on the ProxyColumn that is called when the ProcyColumn methods are being defined to allow custom functionality
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment