0

Here is my problem. I like Andrea Pavoni's way of allowing a nested hash to be used to initialize a class.

require 'ostruct'

class DeepStruct < OpenStruct
  def initialize(hash=nil)
    @table = {}
    @hash_table = {}

    if hash
      hash.each do |k,v|
        @table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
        @hash_table[k.to_sym] = v

        new_ostruct_member(k)
      end
    end
  end

  def to_h
    @hash_table
  end

end

But I can't find a way to include a hash (in the class) with specific default values, so that the behavior would be as follows:

Original behavior without default (with above code):

input_hash = {a: {b: 1}}
new_object = DeepStruct.new hash
new_object.a      # => #<DeepStruct b=1>
new_object.a.b    # => 1
new_object.a.to_h # => {b: 1}

With the following default_h defined inside the class:

default_h = {a: {dc: 2}, dd: {de: 4}}

input_hash and default_h should merge as follows (actually using deep_merge for nested hash)

{:a=>{:dc=>2, :b=>1}, :dd=>{:de=>4}}

The behavior with default hash should be:

new_object = DeepStruct.new hash
new_object.a.b    # => 1
new_object.a.dc   # => 2
new_object.a.to_h # => {:dc=>2, :b=>1}

I can't find a way to implement this behavior inside the class. I would really appreciate any help in this matter.

Edit: Now trying to use David's code in a class:

class CompMedia
    require 'ostruct'
    attr_accessor :merged_h

    def initialize(hash)
        defaults = {a: {dc: 2}, dd: {de: 4}}
      @merged_h = {}
      deep_update(merged_h, defaults)
      deep_update(merged_h, hash)
      @merged_h
    end

    def deep_update(dest, src)
      src.each do |key, value|
        if value.is_a?(Hash)
          dest[key] = {} if !dest[key].is_a?(Hash)
          deep_update(dest[key], value)
        else
          dest[key] = value
        end
      end
    end

    def deep_open_struct(hash)
      result = OpenStruct.new
      hash.each do |key, value|
        if value.is_a?(Hash)
          result[key] = deep_open_struct(value)
        else
          result[key] = value
        end
      end
        result
    end

end # class CompMedia

input_hash = {a: {b: 1}}

cm = CompMedia.new(input_hash)

object = cm.deep_open_struct(cm.merged_h)

p object.marshal_dump    # {:a=>#<OpenStruct dc=2, b=1>, :dd=>#<OpenStruct de=4>}
p object.a               # <OpenStruct dc=2, b=1>
p object.a.marshal_dump  # {:dc=>2, :b=>1}
p object.a.b             # 1
p object.a.dc            # 2
p object.dd              # <OpenStruct de=4>

Obviously, I haven't found a way to retrieve in a simple fashion the nested hash elements from the openstruct object. My objective is to create a class that would be initialized with a default (nested) hash contained in the class, and a (nested) input hash. In addition, I want to be able to add methods that would process the hash inside the class. I am not there yet.

On the other hand, I could just use the merged hash and this would work albeit with slightly more cumbersome notations:

class CompMedia
    attr_accessor :merged_h

    def initialize(hash)
        defaults = {a: {dc: 2}, dd: {de: 4}}
      @merged_h = {}
      deep_update(merged_h, defaults)
      deep_update(merged_h, hash)
      @merged_h
    end

    def deep_update(dest, src)
      src.each do |key, value|
        if value.is_a?(Hash)
          dest[key] = {} if !dest[key].is_a?(Hash)
          deep_update(dest[key], value)
        else
          dest[key] = value
        end
      end
    end

    def multiply_by(k)
        merged_h[:a][:dc] * k
    end

end

input_hash = {a: {b: 1}}

cm = CompMedia.new(input_hash)
p cm.merged_h           # {:a=>{:dc=>2, :b=>1}, :dd=>{:de=>4}}
p cm.merged_h[:a]       # {:dc=>2, :b=>1}
p cm.merged_h[:a][:dc]  # 2
p cm.merged_h[:dd]      # {:de=>4}
p cm.multiply_by(10)    # 20

I will consider the last version as my solution unless someone can make the code with OpenStruct work, which I would prefer.

Community
  • 1
  • 1
JMor
  • 47
  • 1
  • 11

1 Answers1

1

Here is some code that does what you want, except I threw away the idea of subclassing OpenStruct because I wasn't sure if it was a good idea. Also, I implemented deep_merge myself because it was pretty easy to do, but you could try using the version from ActiveSupport if you wanted.

require 'ostruct'

# Merges two hashes that could have hashes inside them.  Default
# values/procs of the input hashes are ignored.  The output hash will
# not contain any references to any of the input hashes, so you don't
# have to worry that mutating the output will affect the inputs.
def deep_merge(h1, h2)
  result = {}
  deep_update(result, h1)
  deep_update(result, h2)
  result
end

def deep_update(dest, src)
  src.each do |key, value|
    if value.is_a?(Hash)
      dest[key] = {} if !dest[key].is_a?(Hash)
      deep_update(dest[key], value)
    else
      dest[key] = value
    end
  end
end

def deep_open_struct(hash)
  result = OpenStruct.new
  hash.each do |key, value|
    if value.is_a?(Hash)
      result[key] = deep_open_struct(value)
    else
      result[key] = value
    end
  end
  result
end

input_hash = {a: {b: 1}}
defaults = {a: {dc: 2}, dd: {de: 4}}
object = deep_open_struct(deep_merge(defaults, input_hash))
p object.a.b
p object.a.dc
p object.a.to_h
David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • Thanks, this works, but I believe I need a class that contains the default hash, and is initialized with the merged hash (default + input). The reason is that I want to include additional methods that process the content of the hash. I am working on it with your code and will post it if I find something. Why is it a bad idea to subclass OpenStruct? – JMor Jul 29 '16 at 20:25
  • It shouldn't be too hard to have a class that holds the default hash. You can leave the code in this post as-is, and make a class that uses it internally. – David Grayson Jul 29 '16 at 20:27
  • I did that. But I am struggling with the methods of that class that would access the merged hash – JMor Jul 29 '16 at 20:30
  • Store the hash in an instance variable, add a method like `def hash; @hash; end`. – David Grayson Jul 29 '16 at 20:54
  • I still have some issues. I will post them today. – JMor Aug 03 '16 at 14:16