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
"/#Action" Route in Routes.Rb in Ruby on Rails
Uploading a File to a S3 Presigned Url
Loop Within Loop in Rails Controller
Ruby Selenium Web Driver: How to Count Child Element Nodes of a Specific Node
Confused with Ruby Accessor Methods
How to Dry Up Method with Multiple { 'Not Found' }
How to Implement Editing/Formating Text Area in Rails
Rails Adding Multiple Objects to an Empty Array
How to Specify Formatting Options for To_Yaml in Ruby
How to Split String into 2 Parts After Certain Position
Rails: Organizing Models in Subfolders Having Warning: Toplevel Constant a Referenced by B::A
Ruby - Convert Integer to String
How to Interpolate a Variable in a Ruby Regex
When Should I Use Datetime VS Date, Time Fields in Ruby/Rails
Return True Only If All Values Evaluate to True in Ruby
Openssl::Cipher::Ciphererror When Running Staging Db on Local