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.

Labels: , ,

Who we are:
The Pasadena Ruby Brigade is a group of programmers who are interested in Ruby. We enjoy sharing our ideas and code with anyone who is interested in this wonderful programming language.

Who can join:
Anyone! Just head over to our mailing list and drop us an email.

What we do:
We recently started a project over at RubyForge. This project is a group of programs written and maintained by our members that we thought could be beneficial to the whole community.

Projects

Downloads

Recent Posts

Archives