Reputation: 8041
Typically, parsing XML or JSON returns a hash, array, or combination of them. Often, parsing through an invalid array leads to all sorts of TypeError
s, NoMethodError
s, 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
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
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
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
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
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