Metaprogramming Tricks, Part 1 - Method’s Metadata in Ruby

By budu

I’ve found out while working with Clojure in my free time that it can be really useful to have metadata bound to subroutines. It opens up a whole lot of possibilities and I recently had a problem in one of our projects where that concept would provide an elegant solution.

Basically, we had a generic presenter which purpose is to prepare a model’s data to be consumed by the DataTables jQuery plugin. It was quite simple at first and could only handle database’s columns.

def fetch
  @results = @model
    .send(@options[:scope] || :scoped)
    .order("#{sort_column} #{sort_direction}")
  # ...
end

This limitation quickly became an issue as there’s always some information which need some kind of formatting or that merge two or more columns. A good example is the name column, to be able to talk to external systems which separate it into a first and last name, we have to keep that data in two distinct columns. In our application though, it make more sense to show them as a single sortable column.

So here we are, the presenter needs to know about the columns used by a model’s method. I decided to look for existing solutions and ended up rolling my own as this required only a few lines of code:

module Metaable
  extend ActiveSupport::Concern

  included do
    class_attribute :metadata

    def self.meta(*args)
      method, data = args
      self.metadata ||= {}
      self.metadata[method] = data if method
      metadata
    end
  end
end

That’s all! We are using a single method to set and retrieve the metadata. Those metadata are kept inside hash map in a class attribute. You can include this mixin inside any ruby classes. In our case we only use it for ActiveRecord’s models so we could add the following to an initializer:

ActiveRecord::Base.send :include, Metaable

Now, we can use the meta method as follow:

def name
  [civility, first_name, last_name]
    .clean_join(' ')
    .titleize
end
meta :name, columns: [:first_name, :last_name]

We simply call it after defining the method and pass it a hash map with a single entry. In this example, we didn’t included the civility attribute as we don’t need it while sorting or searching.

Finally, we can improve our presenter to be more flexible, I’ve extracted the order_clause method from fetch and it look like this:

def order_clause
  if @model.respond_to? :meta
    columns = (@model.meta[sort_column] || {})[:columns]
  end

  if columns
    columns.map { |c| "#{c} #{sort_direction}" }.join(',')
  elsif @model.columns.select { |c| c.name == sort_column.to_s }.present?
    "#{sort_column} #{sort_direction}"
  end
end

This new code is much more useful and safer thanks to a bit a metaprogramming!