Fork me on GitHub
Subscribe to RSS Feed

Elijah Miller

01 Feb 2009

The Making of typed_serialize

My typed_serialize plugin came from the repetition of code just like this.

serialize :options, Hash

def options
  value = super
  if value.is_a?(Hash)
    value
  else
    self.options = {}
  end
end

It calls super to peek at what ActiveRecord would return for the serialized column. If it’s a Hash, we just return it right away. If it’s anything else we set it to a new Hash and return that.

Distilling the interface

After thinking about the pattern for a bit, I decided that simplest shorthand would be this.

class User < ActiveRecord::Base
  typed_serialize :options, Hash
end

This code says “there is a typed and serialized attribute named options, that will always be a Hash.” Notice that it is nearly the same usage as the original serialize method.

Get to it

First off, we define a method that is accessible at the time of class definition. Since the usage is the same as serialize, we can use the original serialize method definition as a starting point.

class ActiveRecord::Base
  def self.typed_serialize(attr_name, class_name = Object)
  end
end

On second thought, what’s the point of class_name being optional? It made sense for the original serialize method, but not typed_serialize. Let’s make class_name mandatory.

class ActiveRecord::Base
  def self.typed_serialize(attr_name, class_name)
  end
end

OK, now our User model can properly execute, but it does absolutely nothing. So let’s at least call Rails’ serialize method to get the standard behavior.

class ActiveRecord::Base
  def self.typed_serialize(attr_name, class_name = Object)
    serialize(attr_name, class_name)
  end
end

Adding the meat

Our repeated code revolved around a custom reader for a serialized attribute. So let’s add a custom reader for attr_name using define_method and our original repeated code.

class ActiveRecord::Base
  def self.typed_serialize(attr_name, class_name = Object)
    serialize(attr_name, class_name)

    define_method(attr_name) do
      value = super
      if value.is_a?(Hash)
        value
      else
        self.options = {}
      end
    end
  end
end

The original code has a couple of small problems when inserted into this context. It assumes the value should always be a Hash and written attribute is always named options.

A quick look at serialize’s implementation tells us it stores its data in a hash with the key as the attribute name in string form, and the value is the class_name. We’ll use that to derive the expected class.

expected_class = self.class.serialized_attributes[attr_name.to_s]

We’ll use Ruby’s send method to call a method with a name we won’t know until runtime.

send("#{attr_name}=", expected_class.new)

All together now.

class ActiveRecord::Base
  def self.typed_serialize(attr_name, class_name = Object)
    serialize(attr_name, class_name)

    define_method(attr_name) do
      expected_class = self.class.serialized_attributes[attr_name.to_s]

      value = super
      if value.is_a?(expected_class)
        value
      else
        send("#{attr_name}=", expected_class.new)
      end
    end
  end
end

This is my first post detailing an implementation. Interestingly enough, it alerted me to a few unnecessarily complex portions of even this tiny amount of code.