'Extract value from optional nested object

How to extract value with static key (:value) in situation when we have object with one of optional nested objects?

message_obj = {
  'id': 123456,
  'message': {
    'value': 'some value',
  }
}

callback_obj = {
  'id': 234567,
  'callback': {
    'value': 'some value',
  }
}

In this situation, I using next instruction:

some_obj[:message] ? some_obj[:message][:value] : some_obj[:callback][:value]

How to extract value from nested object, then we know list of acceptable objects names (eg. [:message, :callback, :picture, ...]). In parent object exist only one nested object.



Solution 1:[1]

I would use Hash#values_at and then pick the value the one hash that was returned:

message
  .values_at(*[:message, :callback, :picture, ...])
  .compact
  .first[:value]

Solution 2:[2]

You could use dig

For example:

message_obj = {
  'id': 123456,
  'message': {
    'value': 'some message value',
  }
}

callback_obj = {
  'id': 234567,
  'callback': {
    'value': 'some callback value',
  }
}

objects = [message_obj, callback_obj]

objects.each  do |obj|
    message_value = obj.dig(:message, :value)
    callback_value = obj.dig(:callback, :value)
    puts "found a message value #{message_value}" if message_value
    puts "found a callback value #{callback_value}" if callback_value
end

This would print:

found a message value some message value
found a callback value some callback value

The nice thing about dig is the paths can be any length, for example the following would also work.

objects = [message_obj, callback_obj]
paths = [
    [:message, :value],
    [:callback, :value],
    [:foo, :bar],
    [:some, :really, :long, :path, :to, :a, :value]
]

objects.each do |obj|
    paths.each do |path|
        value = obj.dig(*path)
        puts value if value
    end
end

Solution 3:[3]

Use Ruby's Pattern-Matching Feature with Hashes

This is a great opportunity to use the pattern matching features of Ruby 3. Some of these features were introduced as experimental and changed often in the Ruby 2.7 series, but most have now stabilized and are considered part of the core language, although I personally expect that they will continue to continue to grow and improve especially as they are more heavily adopted.

While still evolving, Ruby's pattern matching allows you to do things like:

objects = [message_obj, callback_obj, {}, nil]

objects.map do
  case _1
  in message: v
  in callback: v
  else v = nil
  end
  v.values.first if v
end.compact
#=> ["some message value", "some callback value"]

You simply define a case for each Hash key you want to match (very easy with top-level keys; a little harder for deeply-nested keys) and then bind them to a variable like v. You can then use any methods you like to operate on the bound variable, either inside or outside the pattern-matching case statement. In this case, since all patterns are bound to v, it makes more sense to invoke our methods on whatever instance of v was found In your example, each :value key has a single value, so we can just use #first or #pop on v.values to get the results we want.

I threw in an else clause to set v to avoid NoMatchingPatternError, and a nil guard in the event that v == nil, but this is otherwise very straightforward, compact, and extremely extensible. Since I expect pattern matching, especially for Hash-based patterns, to continue to evolve in the Ruby 3 series, this is a good way to both explore the feature and to take a fairly readable and extensible approach to what might otherwise require a lot more looping and validation, or the use of a third-party gem method like Hashie#deep_find. Your mileage may vary.

Caveats

As of Ruby 3.1.1, the ability to use the find pattern on deeply-nested keys is somewhat limited, and the use of variable binding when using the alternates syntax currently throws an exception. As this is a fairly new feature in core, keep an eye on the changelog for Ruby's master branch (and yes, future readers, the branch is still labeled "master" at the time of this writing) or on the release notes for the upcoming Ruby 3.2.0 preview and beyond.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 spickermann
Solution 2
Solution 3 Todd A. Jacobs