Custom To_Yaml and Domain_Type

Custom to_yaml and domain_type

OK, here's what I came up with

class Person

def to_yaml_type
"!example.com,2010-11-30/person"
end

def to_yaml(opts = {})
YAML.quick_emit( nil, opts ) { |out|
out.scalar( taguri, to_string_representation, :plain )
}
end

def to_string_representation
...
end

def Person.from_string_representation(string_representation)
... # returns a Person
end
end

YAML::add_domain_type("example.com,2010-11-30", "person") do |type, val|
Person.from_string_representation(val)
end

Ruby yaml custom domain type does not keep class

It look like you’re using the older Syck interface, rather that the newer Psych. Rather than using to_yaml and YAML.quick_emit, you can use encode_with, and instead of add_domain_type use add_tag and init_with. (The documentation for this is pretty poor, the best I can offer is a link to the source).

class Duration
def to_yaml_type
"tag:example.com,2012-06-28/duration"
end

def encode_with coder
coder.represent_scalar to_yaml_type, to_string_representation
end

def init_with coder
split = coder.scalar.split ":"
initialize(:hours => split[0], :minutes => split[1], :seconds => split[2])
end

def to_string_representation
format("%h:%m:%s")
end

def Duration.from_string_representation(string_representation)
split = string_representation.split(":")
Duration.new(:hours => split[0], :minutes => split[1], :seconds => split[2])
end
end

YAML.add_tag "tag:example.com,2012-06-28/duration", Duration

p s = YAML.dump(Duration.new(27500))
p YAML.load s

The output from this is:

"--- !<tag:example.com,2012-06-28/duration> 7:38:20\n...\n"
#<Duration:0x00000100e0e0d8 @seconds=20, @total=27500, @weeks=0, @days=0, @hours=7, @minutes=38>

(The reason the result you’re seeing is the total number of seconds in the Duration is because it is being parsed as sexagesimal integer.)

Adding #to_yaml to DataMapper models

Using to_yaml is deprecated in Psych, and from my testing it seems to be actually broken in cases like this.

When you call to_yaml directly on your object, your method gets called and you get the result you expect. When you call it on the array containing your object, Psych serializes it but doesn’t correctly handle your to_yaml method, and ends up falling back onto the default serialization. In your case this results in an attempt to serialize an anonymous Class which causes the error.

To fix this, you should use the encode_with method instead. If it’s important that the serialized form is tagged as an OpenStruct object in the generated yaml you can use the represent_object (that first nil parameter doesn’t seem to be used):

def encode_with(coder)
mini_me = OpenStruct.new
instance_variables.each do |var|
next if /^@_/ =~ var.to_s
mini_me.send("#{var.to_s.gsub(/^@/, '')}=", instance_variable_get(var))
end

coder.represent_object(nil, mini_me)
end

If you were just using OpenStruct for convenience, an alternative could be something like:

def encode_with(coder)
instance_variables.each do |var|
next if /^@_/ =~ var.to_s
coder[var.to_s.gsub(/^@/, '')]= instance_variable_get(var)
end
end

Note that Datamapper has its own serializer plugin that provides yaml serialization for models, it might be worth looking into.

Extending Hash and (de)serializing from/to yaml

As Mladen Jablanović's answer shows, you can override to_yaml. You could add an array named 'attributes' (taking special care to escape that name if there is a key in the hash with that name (taking care to escape the escaped name if ... etc.)). However, you need some knowledge of the internals to make this work (the out.map(tag_uri, to_yaml_style) and its variations are nontrivial and not well documented: the sources of the various Ruby interpreters are your best bet).

Unfortunately, you also need to override the deserialization process. How you can reuse existing code there is close to completely undocumented. As in this answer, you see you would need to add a to_yaml_type and add the deserialization code using YAML::add_domain_type. From there, you are pretty much on your own: you need to write half a YAML parser to parse the yamled string and convert it into your object.

It's possible to figure it out, but the easier solution, that I implemented last time I wanted this, was to just make the Hash an attribute of my object, instead of extending Hash. And later I realized I wasn't actually implementing a subclass of Hash anyway. That something is storing key-value pairs doesn't necessarily mean it is a Hash. If you implement :[], :[]= and each, you usually get a long way towards being able to treat an object as if it is a Hash.

Inconsistent wrapping of quotes in to_yaml

  • Column 1, section 2, text contains a trailing space.
  • Column 2, section 8, text contains a : character followed by a space.

The specification says:

The plain (unquoted) style has no identifying indicators and provides no form of escaping. It is therefore the most readable, most limited and most context sensitive style. In addition to a restricted character set, a plain scalar must not be empty, or contain leading or trailing white space characters. It is only possible to break a long plain line where a space character is surrounded by non-spaces.

...

Plain scalars must never contain the “: ” and “ #” character combinations. Such combinations would cause ambiguity with mapping key: value pairs and comments. In addition, inside flow collections, or when used as implicit keys, plain scalars must not contain the “[”, “]”, “{”, “}” and “,” characters. These characters would cause ambiguity with flow collection structures.

How do I deserialize YAML documents from external sources and have full access on class members?

For me sample_car in the IRB shell evaluates to:

=> #<Syck::DomainType:0x234df80 @domain="yaml.org,2002", @type_id="Car", @value={"brand"=>"Porsche", "color"=>"red", "extra_equipment"=>["sun roof", "air conditioning"], "horsepower"=>180}>

Then I issued sample_car.value:

=> {"brand"=>"Porsche", "color"=>"red", "extra_equipment"=>["sun roof", "air conditioning"], "horsepower"=>180}

Which is a Hash. This means, that you can construct your Car object by adding a class method to Car like so:

def self.from_hash(h)
Car.new(h["brand"], h["horsepower"], h["color"], h["extra_equipment"])
end

Then I tried it:

porsche_clone = Car.from_hash(sample_car.value)

Which returned:

=> #<Car:0x236eef0 @brand="Porsche", @horsepower=180, @color="red", @extra_equipment=["sun roof", "air conditioning"]>

That's the ugliest way of doing it. There might be others. =)

EDIT (19-May-2011): BTW, Just figured a lot easier way:

def from_hash(o,h)
h.each { |k,v|
o.send((k+"=").to_sym, v)
}
o
end

For this to work in your case, your constructor must not require parameters. Then you can simply do:

foreign_car = from_hash(Car.new, YAML::load(File.open("foreign_car.yaml")).value)
puts foreign_car.inspect

...which gives you:

#<Car:0x2394b70 @brand="Porsche", @color="red", @extra_equipment=["sun roof", "air conditioning"], @horsepower=180>


Related Topics



Leave a reply



Submit