Metaprogramming Tricks, Part 1 - Method’s Metadata in Ruby
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!