kotu b
kotu b

Reputation: 333

How to generate direct access keys to nested hash which contains hash and arrays as values?

I want to compare two XML files where one is input and the other is output. I am converting both into a hash.

My idea is to get all the keys from the input XML converted to hash, and search each key in both the input and output hashes for their respective key/value pairs.

I have a hash:

{
  "requisition_header" => {
    "requested_by" => {"login" => "coupasupport"},
    "department" => {"name" => "Marketing"},
    "ship_to_address" => {"name" => "Address_1431693296"},
    "justification" => nil,
    "attachments" => [],
    "requisition_lines" => [
      {
        "description" => "Cleaning Services for Building A",
        "line_num" => 1,
        "need_by_date" => 2010-09-23 07:00:00 UTC,
        "source_part_num" => nil,
        "supp_aux_part_num" => nil,
        "unit_price" => #<BigDecimal:a60520c,'0.3E4',9(18)>,
        "supplier" => {"name" => "amazon.com"},
          "account" => {
          "code" => "SF-Marketing-Indirect",
          "account_type" => {"name" => "Ace Corporate"}
        },
        "currency" => {"code" => "USD"},
        "payment_term" => {"code" => "Net 30"},
        "shipping_term" => {"code" => "Standard"},
        "commodity" => {"name" => "Marketing-Services"}
      }
    ]
  }
}

It is nested and all the values are not directly accessible.

I want a way to generate direct access to each value in the hash.

For example:

requisition_header.requested_by.login

will access "coupasupport".

requisition_header.department.name

will access "Marketing".

requisition_header.requisition_lines[0].description

will access "Cleaning Services for Building A".

requisition_header.requisition_lines[0].line_num

will access "1".

requisition_header.requisition_lines[0].need_by_date

will access "2010-09-23 07:00:00 UTC".

Each key built can be used to search for the value directly inside the hash.

Upvotes: 1

Views: 389

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110725

You could use BasicObject#method_missing:

Code

class Hash
  def method_missing(key,*args)
    (args.empty? && key?(key)) ? self[key] : super
  end
end

Example

hash = { animals: {
           pets: { dog: "Diva", cat: "Boots", python: "Stretch" },
           farm: { pig: "Porky", chicken: "Little", sheep: "Baa" }
         },
         finishes: {
           tinted: { stain: "Millers", paint: "Oxford" },  
           clear:  { lacquer: "Target", varnish: "Topcoat" }
         }
       }

hash.finishes.tinted.stain
  #=> "Millers
hash.animals.pets.cat
  #=> "Boots"
hash.animals.pets
  #=> {:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"}
hash.animals
  #=> {:pets=>{:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"},
  #    :farm=>{:pig=>"Porky", :chicken=>"Little", :sheep=>"Baa"}} 

Reader challenge

There is a potential "gotcha" with this approach. I leave it to the reader to identify it. My example contains a clue. (Mind you, there may be other problems I haven't thought of.)

Upvotes: 0

Sharvy Ahmed
Sharvy Ahmed

Reputation: 7405

You can do it by overriding OpenStruct#new as well,

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

Now you can do:

require 'deep_struct'

hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}

mystruct = DeepStruct.new hash
mystruct.requisition_header.requested_by.login # => coupasupport
mystruct.requisition_header.to_h # => {"requested_by"=>{"login"=>"coupasupport"}

Upvotes: 0

spickermann
spickermann

Reputation: 106992

That could be done with the following method, that translates the nested hash into nested OpenStructs:

require 'ostruct'
def deep_structify(hash)
  result = {}
  hash.each do |key, value|
    result[key] = value.is_a?(Hash) ? deep_structify(value) : value
  end if hash
  OpenStruct.new(result)
end

hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}

struct = deep_structify(hash)

struct.requisition_header.department.name
#=> "Marketing"

Upvotes: 3

Related Questions