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.

1 Answer 1

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
Sign up to request clarification or add additional context in comments.

5 Comments

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?
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.
I did that. But I am struggling with the methods of that class that would access the merged hash
Store the hash in an instance variable, add a method like def hash; @hash; end.
I still have some issues. I will post them today.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.