YAML Troubles

So for MapBuzz we have some machines running on Ruby 1.8.2 and 1.8.4. And we use GEOS, a C++ library, for manipulating geometries. These geometries are part of Rails test fixtures, and thus must support custom YAML marshalling. Turns out there were some big changes in YAML between 1.8.2 and 1.8.4. After struggling most of today with this, partially due to lack of documentation, here is what we found.

We’ll use an example of a simple coordinate class that has an x and a y value. What we want to end up with is this:

!ruby/Geos::Geometry
  x: 7
  y: 4

Ruby 1.8.2

For Ruby 1.8.2, the to_yaml_type controls the type that is output in the YAML document (the !ruby/Geos::Geometry bit). You also need to define a method to output YAML (to_yaml naturally enough) and one to read YAML (add_ruby_type strangely).

require 'geos'

class Coordinate  
  def to_yaml_type
    "!ruby/#{self.class}"
  end

  def to_yaml( opts = {} )
    YAML::quick_emit( self.object_id, opts ) do |out|
      out.map(to_yaml_type) do |map|
        ['x','y'].each do |m|
          map.add( m, self.send(m) )
        end
      end
    end
  end
  YAML.add_ruby_type( /^Geos::Coordinate/ ) do |type, val|
    result = Geos::Coordinate.new()
    val.each_pair do |k,v|
      result.send("#{k}=", v)
    end
    result
  end
end

Ruby 1.8.4

Things are significantly different with Ruby 1.8.4. First, the add_ruby_type method has been deprecated in favor of a class method called yaml_new. In fact, as far as I can see the add_ruby_type no longer works. Another major change is the use of taguris, such as “tag:ruby.yaml.org,2002.” In fact, the 1.8.2 form !ruby is a shortcut for a YAML tag.

require 'geos'

class Coordinate  
  yaml_as "tag:ruby.yaml.org,2002:#{self}"

  def to_yaml( opts = {} )
    YAML::quick_emit( self.object_id, opts ) do |out|
      out.map(taguri) do |map|
        ['x','y'].each do |m|
          map.add( m, self.send(m) )
        end
      end
    end
  end

  def self.yaml_new(klass, tag, val)
    result = Geos::Coordinate.new()
    val.each_pair do |k,v|
      result.send("#{k}=", v)
    end
    result
  end
end

Of course, writing code that works on both platforms is kind of a pain.
The approach we are using is:

require 'geos'

class Coordinate  
  yaml_as "tag:ruby.yaml.org,2002:#{self}"

  if not self.respond_to?(:taguri)
    def taguri
      "!ruby/#{self.class}"
    end
    YAML.add_ruby_type( /^Geos::Coordinate/ ) do |type, val|
      Coordinate.yaml_new(Geos::Coordinate, type, val)
    end
  end
    
  def to_yaml( opts = {} )
    YAML::quick_emit( self.object_id, opts ) do |out|
      out.map(taguri) do |map|
        ['x','y'].each do |m|
          map.add( m, self.send(m) )
        end
      end
    end
  end

  def self.yaml_new(klass, tag, val)
    result = Geos::Coordinate.new()
    val.each_pair do |k,v|
      result.send("#{k}=", v)
    end
    result
  end
end

This requires putting this code somewhere:

if not Module.methods.include?('yaml_as')
  # We're on a Ruby version before 1.8.4, stub out yaml_as
  class Module
    def yaml_as( tag, sc = true )
    end
  end
end

Hope this helps!

Leave a Reply

Your email address will not be published. Required fields are marked *

Top