Hash to OpenStruct (#81)

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

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

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

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

My opinion is an OpenStruct, for consistency.

James Edward G. II

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}

On 6/2/06, Jamie M. [email protected] 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

Jamie M. 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.

On Jun 5, 2006, at 1:25 PM, [email protected] wrote:

Thanks,
T.

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

Mat S. 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.

James G. wrote:

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

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

My opinion is an OpenStruct, for consistency.

James Edward G. 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}]}

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 B.

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 B.

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

which isn’t totally obvious.

/O

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.

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.

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

Ola B. 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.

----- Original Message -----
From: [email protected]
Date: Wednesday, June 7, 2006 3:46 pm
Subject: Re: Hash to OpenStruct (#81)
To: [email protected] (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