Custom YAML Emitter
Friday, March 23, 2007 by Nate Murray.
Rational Numbers
Just recently I needed to store a rational
number in a database. YAML is
perfect for this sort of thing. Unfortunately there isn’t a built in to_yaml
for the standard Rational
class.
require 'yaml'
rat = Rational(4,3) # => Rational(4, 3)
rat.to_s # => "4/3"
y = YAML.dump(rat) # => "--- !ruby/object:Rational 4/3\n"
back = YAML.load(y) # => Rational(nil, nil)
Notice that rat
gets emitted as a vanilla ruby object with class Rational
but then the emitter just converts rat
into a string and we get "4/3"
appended to the YAML output. Because the YAML parser doesn’t know what do to
with the string "4/3"
we get back a Rational
object but it doesn’t have its
numerator or denominator set. We want back
to be set to Rational(4, 3)
,
just like the original object.
Register Your Class
What we need to do is register our Rational
class with YAML so that it knows
how to emit and parse our specific type of object.
We can specify our yaml_type
by defining a method to_yaml_type
. We then
register with YAML by calling YAML::add_domain_type
and passing it a block.
The YAML parser will then call this block when it tries to emit an object of
this matching type.
Notice below that YAML::add_domain_type
yields two variables type
and
val
. type
is the YAML type we specified with to_yaml_type
and the val
is the value that was stored during the YAML creation process.
require 'yaml'
class Rational
def to_yaml_type; "!pasadenarb.com,2007-03-23/rational"; end
end
YAML::add_domain_type( "pasadenarb.com,2007-03-23", "rational") do |type, val|
type # => "tag:pasadenarb.com,2007-03-23:rational"
val # => "4/3"
end
rat = Rational(4,3) # => Rational(4, 3)
yam = YAML.dump(rat) # => "--- !pasadenarb.com,2007-03-23/rational 4/3\n"
back = YAML.load(yam) # => "4/3"
Simple Parsing
Notice here that YAML.load
returned the value returned by the block we passed
add_domain_type
. In this case it is val
("4/3"
). We are a little closer to
our goal, but back
is still not a Rational
, it’s a String. What we need to
do is improve on the block we are passing to add_domain_type
.
We are getting the string "4/3"
in val
so we can derive the numerator and
denominator from that string and then return a Rational
number from that
string.
require 'yaml'
class Rational
def to_yaml_type; "!pasadenarb.com,2007-03-23/rational"; end
end
YAML::add_domain_type( "pasadenarb.com,2007-03-23", "rational") do |type, val|
num, den = val.split("\/") # => ["4", "3"]
Rational(num.to_i, den.to_i)
end
rat = Rational(4,3) # => Rational(4, 3)
yam = YAML.dump(rat) # => "--- !pasadenarb.com,2007-03-23/rational 4/3\n"
back = YAML.load(yam) # => Rational(4, 3)
Notice that back
is Rational(4, 3)
, just as we originally wanted.
However this method is not as perfect as it could be. In this case we are able
to derive the attributes we need pretty easily, but what if we had a more
complicated object that did not store all of its attributes if you call #to_s
on the object? What we need is more control over the YAML creation process.
Thankfully that power is available by creating our own #to_yaml
method.
Advanced Emitting
If you look at the #to_yaml
method below you will see that we are iterating
through the instance_variables
and setting the key to be the instance
variable name and the value is the instance variable value.
Then when we need to create the Rational
number from that we just grab the
hash keys from val
.
require 'yaml'
class Rational
def to_yaml_type; "!pasadenarb.com,2007-03-23/rational"; end
def to_yaml( opts = {} )
YAML.quick_emit( self.object_id, opts ) { |out|
out.map( taguri, to_yaml_style ) { |map|
instance_variables.sort.each { |iv|
map.add( iv[1..-1], instance_eval( iv ) )
}
}
}
end
end
YAML::add_domain_type( "pasadenarb.com,2007-03-23", "rational") do |type, val|
num, den = val["numerator"], val["denominator"] # => [4, 3]
Rational(num.to_i, den.to_i)
end
rat = Rational(4,3) # => Rational(4, 3)
yam = YAML.dump(rat) # => "--- !pasadenarb.com,2007-03-23/rational \ndenominator: 3\nnumerator: 4\n"
back = YAML.load(yam) # => Rational(4, 3)
Conclusion
As you can see YAML is a very powerful way to get complex objects into strings.
There are a few other shortcuts to get custom objects into YAML such as
defining #to_yaml_properties
. If you are interested in doing something simple, I’d start by looking here.