From: takashikkbn@... Date: 2019-01-26T14:35:09+00:00 Subject: [ruby-core:91286] [Ruby trunk Feature#15563] #dig that throws an exception if an key doesn't exist Issue #15563 has been updated by k0kubun (Takashi Kokubun). Personally I've hit real-world use-case for this feature many times. I often manage structured configs with nested YAML files and load it from Ruby. With current ruby, to avoid unhelpful exception `NoMethodError`, I assert the existence of the deep keys using a `Hash#fetch` chain like this: ```ruby config = YAML.load_file('config.yml') config.fetch('production').fetch('environment').fetch('SECRET_KEY_BASE') #=> an exception like: KeyError: key not found: "SECRET_KEY_BASE" ``` If we have such a method, we would be able to write (let's say it's named `Hash#fetch_keys` instead of `#dig!`): ```ruby config.fetch_keys('production', 'environment', 'SECRET_KEY_BASE') ``` and its good part is that we could get a more helpful error message like "key not found: production.environment.SECRET_KEY_BASE" whose nest information can't be get with `Hash#fetch` method chains. --- By the way, if we had this, I would like to have a keyword argument `default:` like the second optional argument of `Hash#fetch`: ```ruby env = 'production' # can be 'staging', 'development' config.fetch_keys(env, 'environment', 'SECRET_KEY_BASE', default: '002bbfb0a35d0fd05b136ab6333dc459') ``` we want to safely manage the credentials only for production, so sometimes we don't want to manage credentials in (safely-managed originally-encrypted) YAML file for development environment and just want to return the unsafe thing as a default value. ---------------------------------------- Feature #15563: #dig that throws an exception if an key doesn't exist https://bugs.ruby-lang.org/issues/15563#change-76536 * Author: 3limin4t0r (Johan Wentholt) * Status: Open * Priority: Normal * Assignee: * Target version: ---------------------------------------- Ruby 2.3.0 introduced `#dig` for *Array*, *Hash* and *Struct*. Both *Array* and *Hash* have `#fetch` which does the same as `#[]`, but instead of returning the default value an exception is raised (unless a second argument or block is given). *Hash* also has `#fetch_values` which does the same as `#values_at`, raising an exception if an key is missing. For `#dig` there is no such option. My proposal is to add a method which does the same as `#dig`, but instead of using the `#[]` accessor it uses `#fetch`. This method would look something like this: ```Ruby module DigWithException def dig_e(key, *others) value = fetch(key) return value if value.nil? || others.empty? if value.respond_to?(__method__, true) value.send(__method__, *others) else raise TypeError, "#{value.class} does not have ##{__method__} method" end end end Array.include(DigWithException) Hash.include(DigWithException) ``` The exception raised is also taken from `#dig` (`[1].dig(0, 1) #=> TypeError: Integer does not have #dig method`). I personally have my issues with the name `#dig_e`, but I haven't found a better name yet. There are also a few other things that I haven't thought out yet. 1. Should this method be able to accept a block which, will be passed to the `#fetch` call and recursive `#dig_e` calls? ```Ruby module DigWithException def dig_e(key, *others, &block) value = fetch(key, &block) return value if value.nil? || others.empty? if value.respond_to?(__method__, true) value.send(__method__, *others, &block) else raise TypeError, "#{value.class} does not have ##{__method__} method" end end end Array.include(DigWithException) Hash.include(DigWithException) ``` 2. I currently kept the code compatible with the `#dig` description. > Extracts the nested value specified by the sequence of *key* objects by calling `dig` at each step, returning `nil` if any intermediate step is `nil`. However, with this new version of the method one could consider dropping the *"returning `nil` if any intermediate step is `nil`"* part, since this would be the more strict version. ```Ruby module DigWithException def dig_e(key, *others) value = fetch(key) return value if others.empty? if value.respond_to?(__method__, true) value.send(__method__, *others) else raise TypeError, "#{value.class} does not have ##{__method__} method" end end end Array.include(DigWithException) Hash.include(DigWithException) ``` I'm curious to hear what you guys think about the idea as a whole, the method name and the two points described above. -- https://bugs.ruby-lang.org/ Unsubscribe: