8

Given a hash such as:

AppConfig = {
  'service' => {
    'key' => 'abcdefg',
    'secret' => 'secret_abcdefg'
  },
  'other' => {
    'service' => {
      'key' => 'cred_abcdefg',
      'secret' => 'cred_secret_abcdefg'
    }
  }
}

I need a function to return service/key in some cases and other/service/key in other cases. A straightforward way is to pass in the hash and an array of keys, like so:

def val_for(hash, array_of_key_names)
  h = hash
  array_of_key_names.each { |k| h = h[k] }
  h
end

So that this call results in 'cred_secret_abcdefg':

val_for(AppConfig, %w[other service secret])

It seems like there should be a better way than what I've written in val_for().

JD.
  • 3,005
  • 2
  • 26
  • 37

4 Answers4

9
def val_for(hash, keys)
  keys.reduce(hash) { |h, key| h[key] }
end

This will raise an exception if some intermediate key is not found. Note also that this is completely equivalent to keys.reduce(hash, :[]), but this may very well confuse some readers, I'd use the block.

tokland
  • 66,169
  • 13
  • 144
  • 170
6
%w[other service secret].inject(AppConfig, &:fetch)
sawa
  • 165,429
  • 45
  • 277
  • 381
  • oh, indeed, and you can even write `:fetch`. This will fail also if the last key is not found, but that seems ok for the OP. – tokland Nov 07 '12 at 08:17
  • Your comment makes my answer look like it was written after you added the symbol to proc solution to your answer. – sawa Nov 07 '12 at 13:07
  • I am puzzled, I +1'd your answer and commented it with "oh, indeed" precisely to show that I had forgotten about this `inject` simplification and I wanted to give credit before editing my answer! Apparently I failed in my intention :-( – tokland Nov 07 '12 at 16:13
  • At first, I did not understand what you meant by "even" in the comment. It implies it is compared to something. Then I realized that your comment makes sense only after reading the edited part of your answer. This looked like assuming the reader will look at your answer, then read mine as a variant of it. But now I understand your intention. – sawa Nov 07 '12 at 16:35
  • 1
    Ok. With the "even" I meant you can simplify it a bit more and drop the `&`, `inject` accepts a symbol so the `Symbol#to_proc` trick is not necessary. – tokland Nov 07 '12 at 16:43
1
appConfig = {
  'service' => {
    'key' => 'abcdefg',
    'secret' => 'secret_abcdefg'
  },
  'other' => {
    'service' => {
      'key' => 'cred_abcdefg',
      'secret' => 'cred_secret_abcdefg'
    }
  }
}

def val_for(hash, array_of_key_names)
  eval "hash#{array_of_key_names.map {|key| "[\"#{key}\"]"}.join}"
end

val_for(appConfig, %w[other service secret]) # => "cred_secret_abcdefg"
Andrés
  • 624
  • 7
  • 12
  • Thanks Andres, but eval'ing a string and using escape characters isn't much prettier than what I originally wrote. :\ – JD. Nov 06 '12 at 21:06
1

Ruby 2.3.0 introduced a new method called dig on both Hash and Array that solves this problem entirely.

AppConfig.dig('other', 'service', 'secret')

It returns nil if the key is missing at any level.

user513951
  • 12,445
  • 7
  • 65
  • 82