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!