Forum: Ruby Hash to OpenStruct (#81)

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2006-06-02 17:33
(Received via mailing list)
The three rules of Ruby Quiz:

1.  Please do not post any solutions or spoiler discussion for this quiz
until
48 hours have passed from the time on this message.

2.  Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3.  Enjoy!

Suggestion:  A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby Talk follow the discussion.  Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Hans Fugal

More than a few times I've wished I could get a nice nested OpenStruct
out of
YAML data, instead of the more unwieldy nested hashes. It's mostly a
matter of
style. It's a straightforward task to convert a nested hash structure
into a
nested OpenStruct, but it's the sort of task that you can do a lot of
ways, and
I'll bet some of you can come up with more elegant and/or more efficient
ways
than I have so far.

Here's a sample YAML document to get you started:

	---
	foo: 1
	bar:
	  baz: [1, 2, 3]
	  quux: 42
	  doctors:
	    - William Hartnell
	    - Patrick Troughton
	    - Jon Pertwee
	    - Tom Baker
	    - Peter Davison
	    - Colin Baker
	    - Sylvester McCoy
	    - Paul McGann
	    - Christopher Eccleston
	    - David Tennant
	  a: {x: 1, y: 2, z: 3}
Cb48ca5059faf7409a5ab3745a964696?d=identicon&s=25 unknown (Guest)
on 2006-06-02 20:32
(Received via mailing list)
On Sat, 3 Jun 2006, Ruby Quiz wrote:

>
> style. It's a straightforward task to convert a nested hash structure into a
> 	  quux: 42
> 	    - David Tennant
> 	  a: {x: 1, y: 2, z: 3}

can we make it more realistic?  how bout this a sample data

     ---
     foo: 1
     bar:
       baz: [1, 2, 3]
       quux: 42
       doctors:
         - William Hartnell
         - Patrick Troughton
         - Jon Pertwee
         - Tom Baker
         - Peter Davison
         - Colin Baker
         - Sylvester McCoy
         - Paul McGann
         - Christopher Eccleston
         - David Tennant
       a: {x: 1, y: 2, z: 3}
     table: walnut
     method: linseed oil
     type: contemporary
     id: 1234
     send: fedex

??

-a
Cff9eed5d8099e4c2d34eae663aae87e?d=identicon&s=25 Jacob Fugal (Guest)
on 2006-06-02 20:32
(Received via mailing list)
On 6/2/06, Ruby Quiz <james@grayproductions.net> wrote:
> More than a few times I've wished I could get a nice nested OpenStruct out of
> YAML data, instead of the more unwieldy nested hashes. It's mostly a matter of
> style. It's a straightforward task to convert a nested hash structure into a
> nested OpenStruct, but it's the sort of task that you can do a lot of ways, and
> I'll bet some of you can come up with more elegant and/or more efficient ways
> than I have so far.

First p0st!

Ok, I cheated... Hans pointed me at the rubyquiz site before this
showed up in the list, so I got a head start. But in only 15 minutes,
I've got it solved, with only 18 lines (only 5 of those are in method
bodies), sans whitespace. Golfers... go!

I'd like to thank Hans for a really straightforward (such that I can
do it in my limited time), yet still interesting quiz!

Jacob Fugal
5c2285f675ad99d67216b8d4f547ac8a?d=identicon&s=25 Jamie Macey (Guest)
on 2006-06-02 21:03
(Received via mailing list)
>             - Jon Pertwee
>             - Tom Baker
>             - Peter Davison
>             - Colin Baker
>             - Sylvester McCoy
>             - Paul McGann
>             - Christopher Eccleston
>             - David Tennant
>           a: {x: 1, y: 2, z: 3}

Ahh, a nice, quick one.

I had a 7 line method, that became 6 lines, that became 4 lines...
became 2 really ugly lines if I can include and not count a helper
method to turn mapped Hashes back into Hashes.

My output, reformatted for prettiness (it also handles Ara's additions):

#<OpenStruct
  foo=1,
  bar=#<OpenStruct
    a=#<OpenStruct z=3, x=1, y=2>,
    quux=42,
    doctors=[
      "William Hartnell",
      "Patrick Troughton",
      "Jon Pertwee",
      "Tom Baker",
      "Peter Davison",
      "Colin Baker",
      "Sylvester McCoy",
      "Paul McGann",
      "Christopher Eccleston",
      "David Tennant"
    ],
    baz=[1, 2, 3]
  >
>

- Jamie
Ddbfebb47432f6599da361df6a135c7c?d=identicon&s=25 Adam Shelly (Guest)
on 2006-06-02 21:12
(Received via mailing list)
I was throwing different yaml files at my solution  and I came across
this
sample of valid YAML which doesn't easily fit into an OpenStruct

---
1: for the money
2: for the show
3: to get ready
4: go go go

Is this a valid testcase?

-Adam
Fc784eadb3b54531fdc3d2053db6f83f?d=identicon&s=25 Mat Schaffer (Guest)
on 2006-06-02 21:15
(Received via mailing list)
On Jun 2, 2006, at 2:31 PM, Jacob Fugal wrote:
>> efficient ways
> do it in my limited time), yet still interesting quiz!
>
> Jacob Fugal

I thought we were waiting to announce completion.  I did it in 17
lines, 9 in the body (whitespace included).  Plus a unit test and a
benchmark.  Short and sweet!  Thanks Hans!
-Mat
Fc784eadb3b54531fdc3d2053db6f83f?d=identicon&s=25 Mat Schaffer (Guest)
on 2006-06-02 21:52
(Received via mailing list)
On Jun 2, 2006, at 3:09 PM, Adam Shelly wrote:
> Is this a valid testcase?
I set up my test case to work with strange keys (numbers, OpenStruct
methods), but I'm not sure what the right behavior is.  I see 3
possible behaviors:
1. accept the data and let the client figure out how to get keys like
"methods" back out again. (my choice)
2. Throw an exception when trying to store a key that doesn't map to
a legal, free function name
	- then what if they define the same key twice?
3. Try to remove the functions that are already defined, then redefine
	- can you undefine core ruby functions like 'methods'?
-Mat
912c61d9da47754de7039f4271334a9f?d=identicon&s=25 MenTaLguY (Guest)
on 2006-06-02 22:01
(Received via mailing list)
On Sat, 3 Jun 2006 03:31:05 +0900, "Jacob Fugal" <lukfugl@gmail.com>
wrote:
> Ok, I cheated... Hans pointed me at the rubyquiz site before this
> showed up in the list, so I got a head start. But in only 15 minutes,
> I've got it solved, with only 18 lines (only 5 of those are in method
> bodies), sans whitespace. Golfers... go!

Hold on.  Golfers, can your solutions handle ... this?

---
&verily
lemurs:
  unite: *verily
  beneath:
    - patagonian
    - bread
    - products
thusly: [1, 2, 3, 4]
E34b5cae57e0dd170114dba444e37852?d=identicon&s=25 Logan Capaldo (Guest)
on 2006-06-02 22:50
(Received via mailing list)
On Jun 2, 2006, at 3:59 PM, MenTaLguY wrote:

> &verily
> lemurs:
>   unite: *verily
>   beneath:
>     - patagonian
>     - bread
>     - products
> thusly: [1, 2, 3, 4]
>
>
>

My non-golf solution can't handle that. You sir, are very very sick
(I mean that in the nicest way possible). Also I didn't know YAML
could have cycles (thought it had to be a tree). This gives me a
sinking feeling in the pit of my stomach. If YAML is meant to be used
for serialization, then of course it must support cycles... but this
makes me worry about all the projects that use YAML as a config file.
Fc784eadb3b54531fdc3d2053db6f83f?d=identicon&s=25 Mat Schaffer (Guest)
on 2006-06-03 00:10
(Received via mailing list)
On Jun 2, 2006, at 3:59 PM, MenTaLguY wrote:
> &verily
> lemurs:
>   unite: *verily
>   beneath:
>     - patagonian
>     - bread
>     - products
> thusly: [1, 2, 3, 4]

Wicked test case, MenTaLguY.  I had to think that one through a
couple times to get it right.  I'm up to 21 lines now (whitespace
included, no comments), but I can handle it.  Still not sure how I
can condense it (short of inserting ; anyway).
-Mat
912c61d9da47754de7039f4271334a9f?d=identicon&s=25 MenTaLguY (Guest)
on 2006-06-03 00:22
(Received via mailing list)
On Sat, 3 Jun 2006 05:49:30 +0900, Logan Capaldo
<logancapaldo@gmail.com> wrote:
> If YAML is meant to be used for serialization, then of course it must support
> cycles... but this makes me worry about all the projects that use YAML as a config file.

For most of the configuration files I've seen, I think it'd mostly only
be an issue if you're doing blind recursive transformations of the tree
(as in this case).

Otherwise, in most cases I've seen, there simply isn't any room for an
arbitrarily deep set of nested hashes in the schema -- either you'd get
an error from a hash being in an unexpected place, or that recursive
subtree would simply get ignored.

You might have to think about these sorts of things on rare occasions,
but it's not the end of the world.

-mental
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-06-03 11:02
(Received via mailing list)
On Sat, 2006-06-03 at 04:59 +0900, MenTaLguY wrote:
> lemurs:
>   unite: *verily
>   beneath:
>     - patagonian
>     - bread
>     - products
> thusly: [1, 2, 3, 4]

Ouch, that knocked me back for a bit. I think I've sussed it now
though :) That YAML's a bit of a dark horse, isn't it?
0b561a629b87f0bbf71b45ee5a48febb?d=identicon&s=25 Dave Burt (Guest)
on 2006-06-03 16:00
(Received via mailing list)
Ross Bamford wrote:
> That YAML's a bit of a dark horse, isn't it?

I'm a sky-puncher, too.

Cheers,
Dave
A9b6a93b860020caf9d2d1d58c32478f?d=identicon&s=25 Ross Bamford (Guest)
on 2006-06-04 19:49
(Received via mailing list)
My solution started off from the most basic hash to openstruct
conversion I could think of: OpenStruct.new(some_hash). Those pesky
nested hashes still needed to be dealt with, so I came up with:

class Hash
  def to_ostruct(clz = OpenStruct)
    clz.new Hash[*inject([]){|ar,(k,v)|ar<<k<<(v.to_ostruct(clz) rescue
v)}]
  end
end

This works, but it's very inefficient, it doesn't pass the case
Mentalguy posted, and it doesn't fail well with invalid keys or other
errors. To handle those things, I had to go a bit longer:

class Hash
  def to_ostruct(clz = OpenStruct, cch = {})
    cch[self] = (os = clz.new)
    each do |k,v|
      raise "Invalid key: #{k}" unless k =~ /[a-z_][a-zA-Z0-9_]*/
      os.__send__("#{k}=", v.is_a?(Hash)? cch[v] ||
v.to_ostruct(clz,cch) : v)
    end
    os
  end
end

I chose to fail for invalid keys, rather than introducing potentially
confusing renaming rules or similar. It's still not as efficient as it
might be, but a bit better than the first one.

Neither solution takes into consideration the problems Ara pointed out -
this is the reason for the optional 'clz' parameter to both methods.
Undef'ing methods from OpenStruct turned out to be a non-starter, since
it uses them itself, so I just implemented a simple, naive DumbStruct
that can be used with the to_ostruct methods above:

class DumbStruct
  alias :__iv_set__ :instance_variable_set
  alias :__class__ :class
  instance_methods.each do |m|
    undef_method(m) unless m =~ /^(__|method_missing|inspect|to_s)|\?$/
  end

  def initialize(hsh = {})
    hsh.each { |k,v| method_missing("#{k}=", v) }
  end

  def method_missing(name, *args, &blk)
    if (name = name.to_s) =~ /[^=]=$/
      name = name[0..-2]
      __iv_set__("@#{name}", args.first)
      (class << self; self; end).class_eval { attr_accessor name }
    else
      super
    end
  end
end

Attached are the full files including testcases and a basic benchmark.
Thanks for another fun and interesting quiz :)
5d15f83f0e681b138d95f1ec430868fa?d=identicon&s=25 Joey (Guest)
on 2006-06-04 19:58
(Received via mailing list)
My solution was very simple:
require 'yaml';require 'ostruct';def h(h)h.map{|k,v|h[k]=Hash\
===v ?h(v):v};OpenStruct.new(h)end;puts h(YAML.load($<.read))

This can't deal with the recursion that MentalGuy(sorry for the wrong
capitalisation!) posted.
The following is basically the same code:
class Hash
  def to_ostruct
    copy = {}
    each do |(key,value)|
      if value.class == Hash
        copy[key] = value.to_ostruct
      else
        copy[key] = value
      end
    end
    OpenStruct.new(copy)
  end
end
I tried to look into changing YAML.load to make OpenStruct's instead of
hashes, but I soon gave up on that :)

j`ey
Aee77dba395ece0a04c688b05b07cd63?d=identicon&s=25 Daniel Berger (Guest)
on 2006-06-04 20:26
(Received via mailing list)
Ross Bamford wrote:
> My solution started off from the most basic hash to openstruct
> conversion I could think of: OpenStruct.new(some_hash). Those pesky
> nested hashes still needed to be dealt with, so I came up with:
>
> class Hash
>   def to_ostruct(clz = OpenStruct)
>     clz.new Hash[*inject([]){|ar,(k,v)|ar<<k<<(v.to_ostruct(clz) rescue v)}]
>   end
> end

class OpenStruct
    alias :old_init :initialize
    def initialize(hash=nil)
       old_init(hash.each{ |k,v| hash[k] = self.class.new(v) if
v.is_a?(Hash) })
    end
end

To handle parameters that are the same as existant method names (i.e.
Ara's sample) requires removal of the 'unless' from new_ostruct_member:

def new_ostruct_member(name)
    name = name.to_sym
    meta = class << self; self; end
    meta.send(:define_method, name) { @table[name] }
    meta.send(:define_method, "#{name}=""#{name}=") { |x| @table[name] =
x }
end

Regards,

Dan
E34b5cae57e0dd170114dba444e37852?d=identicon&s=25 Logan Capaldo (Guest)
on 2006-06-04 21:01
(Received via mailing list)
Here's mine:

% cat hash_to_open_struct2.rb
require 'yaml'
require 'ostruct'
class Object
   def hash_to_ostruct(visited = [])
     self
   end
end

class Array
   def hash_to_ostruct(visited = [])
     map { |x| x.hash_to_ostruct(visited) }
   end
end

class Hash
   def hash_to_ostruct(visited = [])
     os = OpenStruct.new
     each do |k, v|
       item = visited.find { |x| x.first.object_id == v.object_id }
       if item
	os.send("#{k}=", item.last)
       else
	os.send("#{k}=", v.hash_to_ostruct(visited + [ [self, os] ]))
       end
     end
     os
   end
end

yaml_source = <<YAML
---
foo: 1
bar:
   baz: [1, 2, 3]
   quux: 42
   doctors:
     - William Hartnell
     - Patrick Troughton
     - Jon Pertwee
     - Tom Baker
     - Peter Davison
     - Colin Baker
     - Sylvester McCoy
     - Paul McGann
     - Christopher Eccleston
     - David Tennant
     - {w: 1, t: 7}
   a: {x: 1, y: 2, z: 3}
YAML
evil_yaml = <<EVIL
---
&verily
lemurs:
   unite: *verily
   beneath:
     - patagonian
     - bread
     - products
thusly: [1, 2, 3, 4]
EVIL

loaded = YAML.load(yaml_source).hash_to_ostruct
p loaded.bar.doctors.last.w

evil_loaded = YAML.load(evil_yaml).hash_to_ostruct
p evil_loaded.lemurs.beneath
p evil_loaded.lemurs.unite.thusly


% ruby hash_to_open_struct2.rb
1
["patagonian", "bread", "products"]
[1, 2, 3, 4]
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-04 21:19
(Received via mailing list)
I manged a very small solution -- practically one line. Only problem
is, it doesn't work ;-) But honestly, it's not my fault! No really. Let
me explain.

When I first read the quiz my thoughts intitally went to the usual
concepts and I considered the Hash#traverse method I wrote some time
ago (BTW this quiz helped me improve that method. Many thanks!) But I
have good bit of experience with YAML and I immediately had a second
thought which would allow me to solve the quiz very quickily and
easily. The solution is as follows (were s containes the yaml sample).

  YAML.add_builtin_type('map'){ |t,v| OpenStruct.new(v) }; o =
YAML.load(s)

But like I said, as clever as it may be, it doesn't work. For whatever
reason Syck doesn't handle it properly. Perhaps YAML's 'map' type is
too fundamental that it can't comply, or perhaps it's a bug. I don't
know. But it just end up returning the same old Hash.

Okay I thought. There's more than one way to skin a cat. And I came up
with this close to one-liner that works around the above problem in a
most clever way.

  i = YAML::load(s)
  def Hash.def to_yaml_type
    "!ruby/object:OpenStruct"
  end
  o = YAML::load(i.to_yaml)

The nice thing about this soluiton is that it uses a built-in library
(YAML/Syck) to do all the hard work --since Syck already understands
graphs it takes care of all those messy issues. Cool.

T.
5da4c52f43677f395aff5bde775593c2?d=identicon&s=25 Daniel Schierbeck (dasch)
on 2006-06-05 00:02
(Received via mailing list)
Joey wrote:
>    end
>    OpenStruct.new(copy)
>  end
> end

This would be cleaner

   class Hash
     def to_ostruct
       copy = dup
       copy.each do |key, value|
         copy[key] = value.to_ostruct if value.respond_to? :to_ostruct
       end
       return copy
     end
   end


Daniel
4674615d2cf231975c741731be9a8685?d=identicon&s=25 why the lucky stiff (Guest)
on 2006-06-05 00:54
(Received via mailing list)
On Mon, Jun 05, 2006 at 02:55:42AM +0900, Joey wrote:
> I tried to look into changing YAML.load to make OpenStruct's instead of
> hashes, but I soon gave up on that :)

Your curiousity shouldn't go unmet.

  require 'yaml'
  require 'ostruct'

  class << YAML::DefaultResolver
    alias_method :_node_import, :node_import
    def node_import(node)
      o = _node_import(node)
      o.is_a?(Hash) ? OpenStruct.new(o) : o
    end
  end

_why
Ef3aa7f7e577ea8cd620462724ddf73b?d=identicon&s=25 Rob Biedenharn (Guest)
on 2006-06-05 02:25
(Received via mailing list)
Gotta post this before I look at other solutions.  This caused me to
look up what an OpenStruct was so that was benefit #1.  Since it was
a simple one, I worked through it with my son who is going through
Chris Pine's _Learning_to_Program_ right now so that was benefit #2.

I don't know if this will handle the crazier recursive YAML files,
but it seems to be fine for normal ones.  My son actually struck on
the OpenStruct#send being a problem with the presence of the 'send'
key in the YAML.

-Rob


# RubyQuiz81: Hash to OpenStruct
# 2006-06-02

require 'ostruct'
require 'yaml'

class HashToOpenStruct
   def self.from_yaml(yamlfile)
     self.to_ostruct(YAML.load(File.open(yamlfile)))
   end

   def self.to_ostruct(h)
     c = OpenStruct.new
     h.each { |k,v| c.__send__("#{k}=".to_sym,
                               v.kind_of?(Hash) ? to_ostruct(v) : v) }
     c
   end
end


__END__

Rob Biedenharn		http://agileconsultingllc.com
Rob@AgileConsultingLLC.com
+1 513-295-4739
E20e89d58211a3631842daecc1245de2?d=identicon&s=25 Ilmari Heikkinen (Guest)
on 2006-06-05 09:30
(Received via mailing list)
> More than a few times I've wished I could get a nice nested OpenStruct out of
> YAML data, instead of the more unwieldy nested hashes. It's mostly a matter of
> style. It's a straightforward task to convert a nested hash structure into a
> nested OpenStruct, but it's the sort of task that you can do a lot of ways, and
> I'll bet some of you can come up with more elegant and/or more efficient ways
> than I have so far.

This one's a bit of a duck:

class Hash
  def method_missing(mn,*a)
    mn = mn.to_s
    if mn =~ /=$/
      super if a.size > 1
      self[mn[0...-1]] = a[0]
    else
      super unless has_key?(mn) and a.empty?
      self[mn]
    end
  end
end
2ef955fe05ff30bd952e90ba2751614d?d=identicon&s=25 Shane Emmons (semmons99)
on 2006-06-05 14:38
Question, should {w: 1, t: 7} be an OpenStruct or remain a hash?

---
foo: 1
bar:
   baz: [1, 2, 3]
   quux: 42
   doctors:
     - William Hartnell
     - Patrick Troughton
     - Jon Pertwee
     - Tom Baker
     - Peter Davison
     - Colin Baker
     - Sylvester McCoy
     - Paul McGann
     - Christopher Eccleston
     - David Tennant
     - {w: 1, t: 7}
   a: {x: 1, y: 2, z: 3}
4299e35bacef054df40583da2d51edea?d=identicon&s=25 James Gray (bbazzarrakk)
on 2006-06-05 15:51
(Received via mailing list)
On Jun 5, 2006, at 7:38 AM, Shane Emmons wrote:

> Question, should {w: 1, t: 7} be an OpenStruct or remain a hash?

My opinion is an OpenStruct, for consistency.

James Edward Gray II
2ef955fe05ff30bd952e90ba2751614d?d=identicon&s=25 Shane Emmons (semmons99)
on 2006-06-05 17:28
James Gray wrote:
> On Jun 5, 2006, at 7:38 AM, Shane Emmons wrote:
>
>> Question, should {w: 1, t: 7} be an OpenStruct or remain a hash?
>
> My opinion is an OpenStruct, for consistency.
>
> James Edward Gray II

Great, then here is my solution along with test code and input file. Not
as small as most solutions, but hopefully understandable. Please let me
know if you spot anything wrong with it.

--- yaml2os.rb ---

#!/usr/local/bin/ruby -w

require 'yaml'
require 'ostruct'

class YAML2OS

  attr_reader :os

  def initialize( file = nil )
    convert(file) if file
  end

  def convert( file )
    yaml = YAML.load(File.open(file))
    @os = hash2os(yaml)
  end

  private

  # Check for hashes and arrays inside 'hash'. Convert any hashes.
  def hash2os( hash )
    hash.each_key do |key|
      hash[key] = hash2os(hash[key]) if hash[key].is_a?(Hash)
      chk_array(hash[key]) if hash[key].is_a?(Array)
    end
    hash = OpenStruct.new(hash)
  end

  # Check for hashes and arrays inside 'array'. Convert any hashes.
  def chk_array( array )
    array.each_index do |i|
      array[i] = hash2os(array[i]) if array[i].is_a?(Hash)
      chk_array(array[i]) if array[i].is_a?(Array)
    end
  end

end

--- tc_yaml2os.rb ---

#!/usr/local/bin/ruby -w

require 'test/unit'

require 'ostruct'

require 'yaml2os'

class TC_YAML2OS < Test::Unit::TestCase

  def setup
    @os = OpenStruct.new
    @os.foo          = 1
    @os.bar          = OpenStruct.new
    @os.bar.baz      = [ 1, 2, OpenStruct.new({'b' => 1, 'c' => 2}),
                         [3, 4, [5, OpenStruct.new({'d' => 3})]] ]
    @os.bar.quux     = 42
    @os.bar.doctors  = [ 'William Hartnell', 'Patrick Troughton',
                         'Jon Pertwee', 'Tom Baker', 'Peter Davison',
                         'Colin Baker', 'Sylvester McCoy', 'Paul
McGann',
                         'Christopher Eccleston', 'David Tennant',
                         OpenStruct.new({'w' => 1, 't' => 7}) ]
    @os.bar.a        = OpenStruct.new({'x' => 1, 'y' => 2, 'z' => 3})
    @os.bar.b        = OpenStruct.new({'a' => [ 1,
                                                OpenStruct.new({'b' =>
2}) ]})

    test_construction
  end

  def test_construction
    @yaml2os = YAML2OS.new('test.yaml')

    assert_not_nil(@yaml2os)
    assert_instance_of(YAML2OS, @yaml2os)
    assert_equal(@os, @yaml2os.os)

    @yaml2os = YAML2OS.new

    assert_not_nil(@yaml2os)
    assert_instance_of(YAML2OS, @yaml2os)
    assert_nil(@yaml2os.os)
  end

  def test_convert
    os = @yaml2os.convert('test.yaml')

    assert_equal(@os, os)
    assert_equal(@os, @yaml2os.os)
  end

end

--- test.yaml ---

---
foo: 1
bar:
   baz: [1, 2, {b: 1, c: 2}, [3, 4, [5, {d: 3}]]]
   quux: 42
   doctors:
     - William Hartnell
     - Patrick Troughton
     - Jon Pertwee
     - Tom Baker
     - Peter Davison
     - Colin Baker
     - Sylvester McCoy
     - Paul McGann
     - Christopher Eccleston
     - David Tennant
     - {w: 1, t: 7}
   a: {x: 1, y: 2, z: 3}
   b: {a: [1, {b: 2}]}
5c2285f675ad99d67216b8d4f547ac8a?d=identicon&s=25 Jamie Macey (Guest)
on 2006-06-05 19:15
(Received via mailing list)
On 6/2/06, Jamie Macey <jamie.macey@gmail.com> wrote:
> >             - Jon Pertwee
>
>     quux=42,
>       "David Tennant"
>     ],
>     baz=[1, 2, 3]
>   >
> >
>
> - Jamie
>

I had two on the go - the clearest code of the ones from Friday (that
just does the simple case) is:

def hash_to_ostruct(hash)
  return hash unless hash.is_a? Hash
  values = {}
  hash.each { |key, value| values[key] = hash_to_ostruct(value) }
  OpenStruct.new(values)
end


To handle MenTaLguY's recursive output, I actually busted in to YAML
for ease of use.

def YAML.load_to_open_struct(yaml)
  hash_to_ostruct(load(yaml))
end

def YAML.hash_to_ostruct(data, memo = {})
  # short-circuit returns so body has less conditionals
  return data unless data.is_a? Hash
  return memo[data.object_id] if memo[data.object_id]

  # log current item in memo hash before recursing
  current = OpenStruct.new
  memo[data.object_id] = current

  # and then recursively populate the current object
  data.each do |key, value|
    current.send(key+'=', hash_to_ostruct(value, memo))
  end
  current
end


Interestingly enough, Facet's Hash#to_ostruct_recurse doesn't seem to
work (stack overflow) for the recursive sample.

- Jamie
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-05 19:25
(Received via mailing list)
Jamie Macey wrote:

> Interestingly enough, Facet's Hash#to_ostruct_recurse doesn't seem to
> work (stack overflow) for the recursive sample.

Yep. It wasn't designed to handle that. If anyone has an *efficient*
fix I add it in. If not I'll just make a note of this limitation in the
docs.

Thanks,
T.
Fc784eadb3b54531fdc3d2053db6f83f?d=identicon&s=25 Mat Schaffer (Guest)
on 2006-06-05 21:05
(Received via mailing list)
On Jun 5, 2006, at 1:25 PM, transfire@gmail.com wrote:
> Thanks,
> T.

Is the obvious case of storing references to previously encountered
hashes really that inefficient?
-Mat
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-05 22:18
(Received via mailing list)
Mat Schaffer wrote:

> Is the obvious case of storing references to previously encountered
> hashes really that inefficient?

Hmm...my initial though is that it would be, but perhaps not since it
is only depth dependent --rare to have a hash with much more than a few
layers of depth. And actually if the list can be passed through the
method interface that would work well (thread safe) and might be useful
in other ways too.

Thanks. I'll try it.

T.
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-05 23:15
(Received via mailing list)
This is new 'facets/core/hash/to_ostruct_recurse':

  require 'ostruct'
  require 'facets/core/ostruct/__update__'

  class Hash

    # Like to_ostruct but recusively objectifies all hash elements as
well.
    #
    #   o = { 'a' => { 'b' => 1 } }.to_ostruct_recurse
    #   o.a.b  #=> 1
    #
    # The +exclude+ parameter is used internally to prevent infinite
    # recursion and is not intended to be utilized by the end-user.
    # But for more advanced usage, if there is a particular subhash you
    # would like to prevent from being converted to an OpenStruct
    # then include it in the exclude hash referencing itself. Eg.
    #
    #     h = { 'a' => { 'b' => 1 } }
    #     o = h.to_ostruct_recurse( { h['a'] => h['a'] } )
    #     o.a['b']  #=> 1
    #

    def to_ostruct_recurse( exclude={} )
      return exclude[self] if exclude.key?( self )
      o = exclude[self] = OpenStruct.new
      h = self.dup
      each_pair do |k,v|
        h[k] = v.to_ostruct_recurse( exclude ) if v.respond_to?(
:to_ostruct_recurse )
      end
      o.__update__( h )
    end

  end

OpenStruct#__update__ is essentially:

  class OpenStruct
    def __update__( other )
      for k,v in hash
        @table[k.to_sym] = v
      end
      self
    end
  end

Any improvements greatly appreciated.

T.
7359ed44852399295c6247dd9719b81b?d=identicon&s=25 Ola Bini (Guest)
on 2006-06-07 11:21
(Received via mailing list)
At 00:53 2006-06-05, you wrote:
>     alias_method :_node_import, :node_import
>     def node_import(node)
>       o = _node_import(node)
>       o.is_a?(Hash) ? OpenStruct.new(o) : o
>     end
>   end
>
>_why

Hi

Another idea would be to use the latest CVS version of RbYAML to do it
like
this, where data is a string or IO to the YAML data:

require 'ostruct'
require 'rbyaml'

RbYAML.add_builtin_ctor("map") {|ctor,node|
   OpenStruct.new(ctor.construct_mapping(node))
}

RbYAML.load(data)


This dosn't really work for mentalguys problem, since RbYAML doesn't
support recursive nodes right now.

Regards
  Ola Bini
7359ed44852399295c6247dd9719b81b?d=identicon&s=25 Ola Bini (Guest)
on 2006-06-07 12:24
(Received via mailing list)
>   OpenStruct.new(ctor.construct_mapping(node))
>}
>
>RbYAML.load(data)
>
>This dosn't really work for mentalguys problem, since RbYAML doesn't
>support recursive nodes right now.
>
>Regards
>  Ola Bini

Correction, CVS RbYAML now handles recursive structures enough to handle
mentalguys problem too.

Another thing that will not work correctly, though, is this map:
---
x: 1
y: 2
z: 3

because the 'y' will be translated to a boolean true, per the YAML spec,
but OpenStruct tries to call to_sym on the keys, which boolean doesn't
handle.
The easiest fix is to class TrueClass; def to_sym; :true end; end and
class
FalseClass; def to_sym; :false end; end
but then this will actually become a struct that looks like this:
#<OpenStruct z=3, x=1, true=2>

which isn't totally obvious.

/O
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-07 13:55
(Received via mailing list)
> because the 'y' will be translated to a boolean true, per the YAML spec,

I'm pretty sure that implict conversion only occurs for values not
keys.

T.
7359ed44852399295c6247dd9719b81b?d=identicon&s=25 Ola Bini (Guest)
on 2006-06-07 13:58
(Received via mailing list)
At 13:53 2006-06-07, you wrote:
> > because the 'y' will be translated to a boolean true, per the YAML spec,
>
>I'm pretty sure that implict conversion only occurs for values not
>keys.
>
>T.

Not correct:

irb(main):002:0> YAML.load("a: b\nyes: false\n")
{"a"=>"b", true=>false}

/O
45196398e9685000d195ec626d477f0e?d=identicon&s=25 unknown (Guest)
on 2006-06-07 15:46
(Received via mailing list)
Ola Bini wrote:
> irb(main):002:0> YAML.load("a: b\nyes: false\n")
> {"a"=>"b", true=>false}

Hmm... I'll have to investigate that. Is it according to the spec?

T.
7359ed44852399295c6247dd9719b81b?d=identicon&s=25 Ola Bini (Guest)
on 2006-06-07 19:57
(Received via mailing list)
----- Original Message -----
From: transfire@gmail.com
Date: Wednesday, June 7, 2006 3:46 pm
Subject: Re: Hash to OpenStruct (#81)
To: ruby-talk@ruby-lang.org (ruby-talk ML)

> >
> > Not correct:
> >
> > irb(main):002:0> YAML.load("a: b\nyes: false\n")
> > {"a"=>"b", true=>false}
>
> Hmm... I'll have to investigate that. Is it according to the spec?
>
> T.
>

Yes, I actually believe so, since it says explicitly in the spec that
mapping keys can be any kinds of object, not even scalar, and
specifically not just str.

Anyway, take a look at the discussion in yaml-core, for a continuation
of this issue.

/O
This topic is locked and can not be replied to.