[QUIZ] Hash to OpenStruct (#81)

Ouch, that knocked me back for a bit. I think I've sussed it now
though :slight_smile: That YAML's a bit of a dark horse, isn't it?

···

On Sat, 2006-06-03 at 04:59 +0900, MenTaLguY wrote:

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]

--
Ross Bamford - rosco@roscopeco.REMOVE.co.uk

Joey wrote:

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

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

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

···

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 :slight_smile:

James Gray 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 ---

···

On Jun 5, 2006, at 7:38 AM, Shane Emmons wrote:

---
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}]}

--
Posted via http://www.ruby-forum.com/\.

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.

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

···

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.

Ross Bamford wrote:

That YAML's a bit of a dark horse, isn't it?

I'm a sky-puncher, too.

Cheers,
Dave

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

···

On Jun 5, 2006, at 1:25 PM, transfire@gmail.com wrote:

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.

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

···

At 00:53 2006-06-05, you wrote:

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 :slight_smile:

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

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.

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

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

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.

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

···

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.

Ola Bini wrote:

···

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}

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

···

----- 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)

Ola Bini wrote:
> 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}

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

T.