Kyle Heironimus
Kyle Heironimus

Reputation: 8041

Easy way to parse hashes and arrays

Typically, parsing XML or JSON returns a hash, array, or combination of them. Often, parsing through an invalid array leads to all sorts of TypeErrors, NoMethodErrors, unexpected nils, and the like.

For example, I have a response object and want to find the following element:

response['cars'][0]['engine']['5L']

If response is

{ 'foo' => { 'bar' => [1, 2, 3] } }

it will throw a NoMethodError exception, when all I want is to see is nil.

Is there a simple way to look for an element without resorting to lots of nil checks, rescues, or Rails try methods?

Upvotes: 2

Views: 237

Answers (6)

Kyle Heironimus
Kyle Heironimus

Reputation: 8041

Since Ruby 2.3, the answer is dig

Upvotes: 0

exbinary
exbinary

Reputation: 1086

For the sake of reference, there are several projects i know of that tackle the more general problem of chaining methods in the face of possible nils:

There's also been considerable discussion in the past:

Having said that, the answers already provided probably suffice for the more specific problem of chained Hash#[] access.

Upvotes: 1

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

I would suggest an approach of injecting custom #[] method to instances we are interested in:

def weaken_checks_for_brackets_accessor inst
  inst.instance_variable_set(:@original_get_element_method, inst.method(:[])) \
    unless inst.instance_variable_get(:@original_get_element_method)

  singleton_class = class << inst; self; end
  singleton_class.send(:define_method, :[]) do |*keys|
    begin
      res = (inst.instance_variable_get(:@original_get_element_method).call *keys)
    rescue
    end
    weaken_checks_for_brackets_accessor(res.nil? ? inst.class.new : res)
  end
  inst
end

Being called on the instance of Hash (Array is OK as all the other classes, having #[] defined), this method stores the original Hash#[] method unless it is already substituted (that’s needed to prevent stack overflow during multiple calls.) Then it injects the custom implementation of #[] method, returning empty class instance instead of nil/exception. To use the safe value retrieval:

a = { 'foo' => { 'bar' => [1, 2, 3] } }

p (weaken_checks_for_brackets_accessor a)['foo']['bar']
p "1 #{a['foo']}"
p "2 #{a['foo']['bar']}"
p "3 #{a['foo']['bar']['ghgh']}"
p "4 #{a['foo']['bar']['ghgh'][0]}"
p "5 #{a['foo']['bar']['ghgh'][0]['olala']}"

Yielding:

#⇒ [1, 2, 3]
#⇒ "1 {\"bar\"=>[1, 2, 3]}"
#⇒ "2 [1, 2, 3]"
#⇒ "3 []"
#⇒ "4 []"
#⇒ "5 []"

Upvotes: 0

sawa
sawa

Reputation: 168121

If you can live with getting an empty hash instead of nil when there is no key, then you can do it like this:

response.fetch('cars', {}).fetch(0, {}).fetch('engine', {}).fetch('5L', {})

or save some types by defining a method Hash#_:

class Hash; def _ k; fetch(k, {}) end end
response._('cars')._(0)._('engine')._('5L')

or do it at once like this:

["cars", 0, "engine", "5L"].inject(response){|h, k| h.fetch(k, {})}

Upvotes: 1

Casper
Casper

Reputation: 34318

I tried to look through both the Hash documentation and also through Facets, but nothing stood out as far as I could see.

So you might want to implement your own solution. Here's one option:

class Hash
  def deep_index(*args)
    args.inject(self) { |e,arg|
      break nil if e[arg].nil?
      e[arg]
    }
  end
end

h1 = { 'cars' => [{'engine' => {'5L' => 'It worked'}}] }
h2 = { 'foo' => { 'bar' => [1, 2, 3] } }

p h1.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('foo', 'bonk')

Output:

"It worked"
nil
nil

Upvotes: 1

peter
peter

Reputation: 42192

Casper was just before me, he used the same idea (don't know where i found it, is a time ago) but i believe my solution is more sturdy

module DeepFetch
  def deep_fetch(*keys, &fetch_default)
    throw_fetch_default = fetch_default && lambda {|key, coll|
      args = [key, coll]
      # only provide extra block args if requested
      args = args.slice(0, fetch_default.arity) if fetch_default.arity >= 0
      # If we need the default, we need to stop processing the loop immediately
      throw :df_value, fetch_default.call(*args)
    }
    catch(:df_value){
      keys.inject(self){|value,key|
        block = throw_fetch_default && lambda{|*args|
          # sneak the current collection in as an extra block arg
          args << value
          throw_fetch_default.call(*args)
        }
        value.fetch(key, &block) if value.class.method_defined? :fetch
      }
    }
  end

  # Overload [] to work with multiple keys
  def [](*keys)
    case keys.size
    when 1 then super
    else deep_fetch(*keys){|key, coll| coll[key]}
    end
  end

end

response = { 'foo' => { 'bar' => [1, 2, 3] } }
response.extend(DeepFetch)

p response.deep_fetch('cars')  { nil } # nil
p response.deep_fetch('cars', 0)  { nil } # nil
p response.deep_fetch('foo')  { nil } # {"bar"=>[1, 2, 3]}
p response.deep_fetch('foo', 'bar', 0)  { nil } # 1
p response.deep_fetch('foo', 'bar', 3)  { nil } # nil
p response.deep_fetch('foo', 'bar', 0, 'engine')  { nil } # nil

Upvotes: 1

Related Questions