From: "Ara.T.Howard" Date: 2007-05-08T06:04:38+09:00 Subject: Re: using YAML as config On Tue, 8 May 2007, Rover Rhubarb wrote: > So I come to the part of my little app where I want to load and save a > config file. I want it to be human read/writeable too though - it's not just > for serialization. In fact, usually a human will write it, my little app > will read it, and may adjust it and write it out anew. > > So in xml it would look like this > > > foo > 3 > > > … > > > I don't care about channels that's just a random root node, and I might > do it with attbs instead of elements, so: > > > > So I thought, lets use YAML instead - it's even easier to read and > write. Google, google, read blogs, docs, google, google, more docs, more > blogs, google to_yaml, fxri: hack hack, google to_yaml_type, fxri: hack, > hack, hack, google, google, yaml_as, hack. > > Argh! Seems like day later, and I'm ready to go back to xml. But I'm > sure there's a simple way to do what I want, so I'll post here first and > hope. See, my class is Channel, like so: > > class Channel; attr_accessor :name, :id; end > > and want my array of channels to look something like this > > - Channel > name: foo > id: 3 > - Channel > name: bar > id: 4 > … > > But I just can't work out how to simply the yaml output to this stage. > And I don't want to override a dozen methods and do my own hash mapping. > The easiest way - thanks to why for posting this in the forum - seems to > be to use yaml_as: > > class Channel > yaml_as "tag:foobar,2007:Channel" > end > > but then I get each entry as > > --- !foolbar,2007/Channel > name: foo > id: 3 > > This is fairly simple, but remember that I want my entries to be > human-writeable the first time. That line of "---!foobar,2007…" is just > way too machiney for us poor humans to get right every time. > > I can simplify the taguri like this > yaml_as "tag::Channel" > > to get > > --- !Channel > ... > > and that's about as simple as I can get it without it failing to write > yaml. But loading it back in will fail to build the object - it will > give ma a YAML::DynamicType instead - which is what it always gives if > it doesn't recognize the object. > I guess the load method needs a valid taguri > > (Side note - I'd never heard of a taguri before, but when I went to read > about it at http://www.taguri.org/ I was _very_ pleasantly surprised at > how quickly I was able to grok the whole thing by reading Sandro Hawke's > four-paragraph tutorial about his dog Taiko. Way to go Sandro - if I'd > been directed to the RFC first it'd be all over) > > So anyway, the fact is that while taguris are great for distinguishing > Taiko from his master's descendents' pooches, and while it's very > simple, it's still overkill for me. I don't need a unique ID. I know > what "--- Channel" means in my little app. I don't want to submit my > Channel types to some great panoply of random little types each with > their own special pet name. I just want to damn well read and write it. > > I've even considered parsing the text in to_yaml and YAML.load and > gsubbing "!foobar, 2007/" out and in again. But there's got to be a > better way. So far that better way is looking like xml. but there's got > to be a better rubyful way. > > What is it? there are simpler ways, but here is one that quite simple and also quite extensible: cfp:~ > ruby a.rb - Foo: a: 4 b: 2 - Foo::Bar: a: 40 b: 1 c: 1 # # cfp:~ > cat a.rb # # define some classes which impliment the required '.from_hash' and '#to_hash' # duckface our ObjectList class will expect # class Foo def self.from_hash hash new hash['a'], hash['b'] end def initialize a, b @a, @b = a, b end def to_hash { 'a' => @a, 'b' => @b } end end class Foo::Bar def self.from_hash hash new hash['a'], hash['b'] , hash['c'] end def initialize a, b, c @a, @b, @c = a, b, c end def to_hash { 'a' => @a, 'b' => @b, 'c' => @c } end end # # an ObjectList class adapted to store any objects which respond_to 'to_hash' # and whose class responds_to 'from_hash' # require 'yaml' class ObjectList < ::Array attr 'path' def initialize path load path end def load path @path = path replace YAML.load(( IO.read @path rescue '[]' )) map!{|entry| obj_from_entry entry} end def save path = @path yaml = map{|obj| entry_from_obj obj}.to_yaml open(path, 'w'){|fd| fd.write yaml} end def obj_from_entry entry classname, hash = entry.to_a.first constant_get(classname).from_hash hash end def entry_from_obj obj classname, hash = obj.class.name, obj.to_hash { classname => hash } end def constant_get hierachy ancestors = hierachy.split %r/::/ parent = Object while((child = ancestors.shift)) klass = parent.const_get(child) and parent = klass end klass end end # # setup a object_list object # object_list = ObjectList.new 'object_list' # # store a few instances in the object_list # object_list << Foo.new(4,2) object_list << Foo::Bar.new(40, 1, 1) # # save the object_list # object_list.save # # show that save works # puts IO.read(object_list.path) # # show that loading works # object_list = ObjectList.new 'object_list' p object_list.first p object_list.last -a -- be kind whenever possible... it is always possible. - the dalai lama