Breaking apart inheritable mattr_accessors in HTTParty

A land of sharp of knives

We all know that Ruby provides you with sharp knives. Used with caution they give you great power, but misuse them and you will be remembered at your job long after.

I like some of those, from outside they look like magic, but inside they often are incomprehensible and hard to debug metaprogramming mess.

And in this post, I want to dig into one of those: inheritable attr_accessors.

A little background

A conventional use of HTTParty is to include it in your class . It allows to create nice and simple wrappers around almost any API and perfect for little API gems. Something like.

class ApiClient include HTTParty base_uri "example.com" headers ( content_type: 'application/json' ) def clients get :clients end end

But what if wanted to inherit from ApiClient ? The obvious behavior would be for a child class to inherit parent’s class-defined options.

As you might have already guessed, there is no straightforward way to do this.

Here comes the dragon

module ModuleInheritableAttributes def self . included ( base ) base . extend ( ClassMethods ) end module ClassMethods def mattr_inheritable ( * args ) @mattr_inheritable_attrs ||= [ :mattr_inheritable_attrs ] @mattr_inheritable_attrs += args args . each do | arg | module_eval %(class << self; attr_accessor :#{arg} end) end end def inherited ( subclass ) super @mattr_inheritable_attrs . each do | inheritable_attribute | ivar = "@ #{ inheritable_attribute } " parent_value = instance_variable_get ( ivar ). clone subclass . instance_variable_set ( ivar , parent_value ) if parent_value . respond_to? ( :merge ) method = <<- EOM def self. #{ inheritable_attribute } #{ ivar } = superclass. #{ inheritable_attribute } .merge(Marshal.load(Marshal.dump( #{ ivar } ) ) end EOM subclass . class_eval method end end end end

I modified a code a bit. Instead of Marshal.load(Marshal.dump() we now use a hash_deep_dup borrowed from ActiveSupport . It makes code even more complex and not relevant to our discussion here, so I replaced it.

It’s then used like so

module HTTParty def self . included ( base ) base . include ModuleInheritableAttributes base . mattr_inheritable ( default_options ) base . instance_variable_set ( "@default_options" , {}) end end

As you can see, use of the code is simple and easy. Code that achieves that, however… Well it’s complex, to say the least.

Let’s take a closer look to see how it works

mattr_inheritable

def mattr_inheritable ( * args ) @mattr_inheritable_attrs ||= [ :mattr_inheritable_attrs ] @mattr_inheritable_attrs += args args . each do | arg | module_eval %(class << self; attr_accessor :#{arg} end) end end

Initializing a class instance variable to hold our inheritable attributes including the variable itself. Adding our new attribute to the array Adding accesor to our module. Notice module_eval here, we evaluate our code in the context of the module. So we are adding accessor not to an instance, but to a module

inherited

def inherited ( subclass ) super @mattr_inheritable_attrs . each do | inheritable_attribute | ivar = "@ #{ inheritable_attribute } " parent_value = instance_variable_get ( ivar ). clone subclass . instance_variable_set ( ivar , parent_value ) if parent_value . respond_to? ( :merge ) method = <<- EOM def self. #{ inheritable_attribute } #{ ivar } = superclass. #{ inheritable_attribute } .merge(Marshal.load(Marshal.dump( #{ ivar } ) ) end EOM subclass . class_eval method end end end

Starting to iterate over our inheritable attributes Getting variable value from parent Setting the value to child Checking the value respond to #merge . Here, with class_eval we redefine our attr reader to always get parent values and then merge with them with values of a subclass. It’s needed for cases when parent options were changed after our child class was evaluated.

That’s it 😊

A land of magic

While reading this you might have thought: “God, what a mess!”. And that’s true, but it also allows for some clean code on the user’s side.

Whether “magic” is always bad is a controversial topic. Rails have been criticized for years because of it. However, it is also the reason why Rails became so popular in the first place. It’s the reason why many of us fell in love with Ruby.

So, as with every other decision in programming, there is always a trade-off one has to make. When you create a library your objective is to create an easy to use tool and for that, a little magic is sometimes necessary.

Update 11.09.2019

Janko Marohnić kindly suggested a refactored, simplified version of the code using module builder pattern

class MattrInheritable < Module #:nodoc: def initialize ( * names ) attr_accessor * names define_method :inherited do | subclass | names . each do | name | super ( subclass ) subclass . send ( :" #{ name } =" , send ( name )) subclass . class_eval <<- RUBY def self. #{ name } @ #{ name } = superclass. #{ name } .merge(MattrInheritable.hash_deep_dup(@ #{ name } )) end RUBY end end end

That can be used later used like so