Ruby Ducks - Serialization Duck Type

If you've used Ruby, you've probably heard of duck typing. Programming against common informal interfaces allows you to interact with objects without knowing their exact type. The Ruby standard library is a great place to find these informal interfaces, or ducks. We're going to start off by looking at the serialization duck, see a few examples from the standard library and the wild, and then see how coding against this interface makes the Rails ActiveRecord::Store API very flexible.

Serialization Implementations

Ruby ships with the Marshal library, which provides the canonical mechanism for serializing and deserializing Ruby objects:

h1   = {x: 1, y: 2}
data = Marshal.dump(h1)   #=> "\x04\b{\a:\x06xi\x06:\x06yi\a"
h2   = Marshal.load(data) #=> {:x=>1, :y=>2} 
h1 == h2                  #=> true

The API is simple, Marshal.dump converts an object to a string, and Marshal.load converts a string back to an object. Marshal however isn't the only serialization library that implements dump and load. Ruby ships with two other libraries that support the same interface:

# YAML Serialization
h1   = {x: 1, y: 2}
data = YAML.dump(h1)   #=> "---\n:x: 1\n:y: 2\n" 
h2   = YAML.load(data) #=> {:x=>1, :y=>2} 
h1 == h2               #=> true

# JSON Serialization
h1   = {x: 1, y: 2}
data = JSON.dump(h1)   #=> "{\"x\":1,\"y\":2}"
h2   = JSON.load(data) #=> {"x"=>1, "y"=>2} 
h1 == h2               #=> false

Take a moment and look over those two examples. Although both the YAML and JSON libraries implement the same interface as Marshal, they serialize to different formats. Did you notice that JSON didn't quite behave the same as the other two serializers? The JSON Spec has no notion of Symbols, so the keys in h1 were converted to Strings. When it comes to duck typing, it's the caller's responsibility to evaluate if the object being passed in quacks enough like the duck in question.

Duck typing makes it very easy for other people to implement these informal APIs. For instance the MessagePack gem provides a different serialization format, and implements the dump and load API:

# MessagePack Serialization
h1   = {x: 1, y: 2}
data = MessagePack.dump(h1)   #=> "\x82\xA1x\x01\xA1y\x02"
h2   = MessagePack.load(data) #=> {"x"=>1, "y"=>2} 
h1 == h2                      #=> false

Thanks to this interface you can freely swap between the JSON and MessagePack classes as long as you aren't concerned about the format the data is serialized in.

ActiveRecord::Store

The Rails ActiveRecord::Store module allows you to serialize an object, usually a Hash, into a single column of a database. This is useful for storing sparse data like settings. ActiveRecord::Store takes advantage of the informal serialization duck type by allowing you to swap in any object that responds to dump and load. Because of this interface you can use formats like MessagePack even though neither ActiveRecord::Store nor MessagePack were written to explicitly work together:

# Use the JSON encoder:
class Person < ActiveRecord::Base
  store :settings, coder: MessagePack
end

p = Person.find(1)
p.settings[:show_welcome] = false
p.save!
# =>  UPDATE "people" SET "settings" = ? WHERE "people"."id" = 1
#     [["settings", "\x81\xACshow_welcome\xC2"]]
p.reload.settings
# => {"show_welcome"=>false}

What's interesting here isn't the code that was written to support this feature, but the code that wasn't. Instead of checking whether the :coder option contains a certain class, Rails lets you pass anything in.

By trusting the caller, Rails has gained flexibility.

Implementing the Serialization Duck

Rails doesn't explicitly support MessagePack, but we were able to use it since it implements a common interface. What if we want to support some other format? Imagine you need to share this model with some other system using XML. There's no object that I'm aware of in Rails that implements the serialization interface for XML, but there is support for converting objects to and from Java, so let's write our own.

class XmlSerializer
  ROOT = "hash"

  def self.dump(obj)
    obj.to_xml :root => ROOT
  end

  def self.load(xml)
    Hash.from_xml(xml)[ROOT]
  end
end

class Person < ActiveRecord::Base
  store :settings, coder: XmlSerializer
end

We've slipped our own duck in, let's see what happens when we try to use it:

p = Person.find(1)
p.settings[:show_welcome] = true
p.save!
# =>  UPDATE "people" SET "settings" = ? WHERE "people"."id" = 1  
#     [["settings", "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n  <show-welcome type=\"boolean\">true</show-welcome>\n</hash>\n"]]
p.reload.settings
# => {"show_welcome"=>true}

We just wrote an adapter between ActiveSupport's XML serialization mechanism and the serialization duck type. Now we can use this in many different places, not just in ActiveRecord::Store.

There are many other duck types in Ruby, if you're interested in hunting them down, peruse the Ruby standard library APIs. We'll investigate some more of these in the future.

blog comments powered by Disqus
Monkey Small Crow Small