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.