Question

I have this old code using Sych doing :

yaml_as "tag:yaml.org,2002:#{self}"
def to_yaml(opts = {})
  YAML::quick_emit(self, opts) do |out|
    out.map(taguri, to_yaml_style) do |map|
      map.add('name', name)
      map.add('address', full_address.upcase) if full_address?
    end
  end
end

which outputs something like that :

--- !Contact
name: SMOKE OIL
address: |-
  SMOKE OIL
  1 RUE DE LA PAIX
  75002 PARIS
  FRANCE

Now, I'm upgrading that old code, going to Psych so, I read the doc and did :

yaml_as "tag:yaml.org,2002:#{self}"
def encode_with(coder)
  coder['name'] = name
  coder['address'] = full_address.upcase if full_address?
end

And that does :

--- !Contact
name: SMOKE OIL
address: ! "SMOKE OIL\n1 RUE DE LA PAIX\n75002 PARIS\nFRANCE"

It's nice YAML but, it's supposed to be the output of a whois server, and it's way less readable by humans…

So, I went back to the doc, and looked at the second way of doing things, that is, building an AST. Now, unless I'm not seeing something, nothing explains you how to take the AST you built, and plug it in a way Psych.dump(obj) would still work…

I tried doing (without much hope) :

a = Psych::Nodes::Scalar(full_address.upcase)
a.style = Psych::Nodes::LITTERAL
coder['address'] = a if full_address?

but, obviously, it did not do what I hoped it'd do… I also tried :

def encode_with(coder)
  Psych::Nodes::Mapping.new.tap do |map|
    map.children << Psych::Nodes::Scalar.new("name")
    map.children << Psych::Nodes::Scalar.new(name)
    map.children << Psych::Nodes::Scalar.new("address")
    a = Psych::Nodes::Scalar.new(full_address.upcase)
    a.style = 4
    map.children << a
  end
end

But, I could not see how to plug it into the coder…

Also, the answer needs to work when doing recursive things, this is a Contact objet, but one can ask for a Domain which will contain a few contacts and I want it as DRY as possible :-)

So, anyone has a hint on how to do this ?

Was it helpful?

Solution

If you want to create your own AST then you can’t use Psych.dump. Psych.dump creates its own AST using the Psych defaults. In your case you want to customise the AST creation process.

Looking at the source of Psych.dump you can see it uses a Psych::Visitors::YAMLTree to create the AST. You can subclass this and customise how it handles your Contact class to get the output you want. In particular you need to override the accept method.

Here’s a simple example that just special cases the Contact class:

class MyYAMLTree < Psych::Visitors::YAMLTree
  def accept target
    return super unless target.is_a? Contact

    @emitter.start_mapping(nil, "tag:yaml.org,2002:#{target.class}", false, Psych::Nodes::Mapping::BLOCK)

    @emitter.scalar 'name', nil, nil, true, false, Psych::Nodes::Scalar::ANY
    @emitter.scalar target.name, nil, nil, true, false, Psych::Nodes::Scalar::ANY

    @emitter.scalar 'address', nil, nil, true, false, Psych::Nodes::Scalar::ANY

    #this is the where we make the address string a literal
    @emitter.scalar target.full_address, nil, nil, true, false, Psych::Nodes::Scalar::LITERAL

    @emitter.end_mapping
  end
end

Note that it’s the Psych::Visitors::YAMLTree class that calls encode_with, this will bypass it altogether for your class.

In order to use this, use something like (this is basically a simplified version of Psych.dump using MyYAMLTree):

def my_yaml o
  visitor = MyYAMLTree.new
  visitor << o
  visitor.tree.yaml
end

This is obviously just a simple example, but hopefully it’ll point you in the right direction.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top