diff options
author | Tyler Dooling <tdooling@decisiv.net> | 2014-02-25 20:45:51 -0600 |
---|---|---|
committer | Tyler Dooling <tdooling@decisiv.net> | 2014-04-04 10:28:00 -0500 |
commit | 545afab2cdfb852c718e7b0e96b91696470b1c38 (patch) | |
tree | 97344d692a06281af60b08c67e501c4e2213432e | |
parent | 89d3aac66785e8cb9b17179ea904f79ab34b9888 (diff) | |
download | hashie-545afab2cdfb852c718e7b0e96b91696470b1c38.tar.gz |
Adds deep_fetch extension for nested access.
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 30 | ||||
-rw-r--r-- | lib/hashie.rb | 1 | ||||
-rw-r--r-- | lib/hashie/extensions/deep_fetch.rb | 29 | ||||
-rw-r--r-- | spec/hashie/extensions/deep_fetch_spec.rb | 70 |
5 files changed, 131 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index ff8b498..bf0d5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Next Release +* [#134](https://github.com/intridea/hashie/pull/134): Add deep_fetch extension for nested access - [@tylerdooling](https://github.com/tylerdooling). * Removed support for Ruby 1.8.7 - [@dblock](https://github.com/dblock). * Ruby style now enforced with Rubocop - [@dblock](https://github.com/dblock). * [#107](https://github.com/intridea/hashie/pull/107): Fixed excessive value conversions causing poor performance of deep merge in Hashie::Mash - [@davemitchell](https://github.com/dblock), [@dblock](https://github.com/dblock). @@ -121,6 +121,36 @@ h1.deep_merge(h2) #=> { :x => {:y => [7, 8, 9]}, :z => "xyz" } h2.deep_merge(h1) #=> { :x => {:y => [4, 5, 6]}, :z => [7, 8, 9] } ``` +### DeepFetch + +This extension can be mixed in to provide for safe and concise retrieval of +deeply nested hash values. In the event that the requested key does not exist +a block can be provided and its value will be returned. + +Though this is a hash extension, it conveniently allows for arrays to be +present in the nested structure. This feature makes the extension particularly +useful for working with JSON API responses. + +```ruby +user = { + name: { first: 'Bob', last: 'Boberts' }, + groups: [ + { name: 'Rubyists' }, + { name: 'Open source enthusiasts' } + ] +} +user.extend Hashie::Extensions::DeepFetch + +user.deep_fetch :name, :first #=> 'Bob' +user.deep_fetch :name, :middle #=> 'KeyError: Could not fetch middle' + +# using a default block +user.deep_fetch :name, :middle { |key| 'default' } #=> 'default' + +# a nested array +user.deep_fetch :groups, 1, :name #=> 'Open source enthusiasts' +``` + ## Mash Mash is an extended Hash that gives simple pseudo-object functionality diff --git a/lib/hashie.rb b/lib/hashie.rb index 6c397e5..4975d3e 100644 --- a/lib/hashie.rb +++ b/lib/hashie.rb @@ -19,5 +19,6 @@ module Hashie autoload :MethodWriter, 'hashie/extensions/method_access' autoload :StringifyKeys, 'hashie/extensions/key_conversion' autoload :SymbolizeKeys, 'hashie/extensions/key_conversion' + autoload :DeepFetch, 'hashie/extensions/deep_fetch' end end diff --git a/lib/hashie/extensions/deep_fetch.rb b/lib/hashie/extensions/deep_fetch.rb new file mode 100644 index 0000000..d8746cb --- /dev/null +++ b/lib/hashie/extensions/deep_fetch.rb @@ -0,0 +1,29 @@ +module Hashie + module Extensions + # Searches a deeply nested datastructure for a key path, and returns the associated value. + # + # options = { user: { location: { address: '123 Street' } } } + # options.deep_fetch :user, :location, :address #=> '123 Street' + # + # If a block is provided its value will be returned if the key does not exist. + # + # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value' + # + # This is particularly useful for fetching values from deeply nested api responses or params hashes. + module DeepFetch + class UndefinedPathError < StandardError; end + + def deep_fetch(*args, &block) + args.reduce(self) do |obj, arg| + begin + arg = Integer(arg) if obj.kind_of? Array + obj.fetch(arg) + rescue ArgumentError, IndexError => e + break block.call(arg) if block + raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace + end + end + end + end + end +end diff --git a/spec/hashie/extensions/deep_fetch_spec.rb b/spec/hashie/extensions/deep_fetch_spec.rb new file mode 100644 index 0000000..7026020 --- /dev/null +++ b/spec/hashie/extensions/deep_fetch_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +module Hashie + module Extensions + describe DeepFetch do + subject { Class.new(Hash) { include Hashie::Extensions::DeepFetch } } + let(:hash) do + { + library: { + books: [ + { title: 'Call of the Wild' }, + { title: 'Moby Dick' } + ], + location: { + address: '123 Library St.' + } + } + } + end + let(:instance) { subject.new.update(hash) } + + describe '#deep_fetch' do + it 'extracts a value from a nested hash' do + instance.deep_fetch(:library, :location, :address).should eq('123 Library St.') + end + + it 'extracts a value from a nested array' do + instance.deep_fetch(:library, :books, 1, :title).should eq('Moby Dick') + end + + context 'when one of the keys is not present' do + context 'when a block is provided' do + it 'returns the value of the block' do + value = instance.deep_fetch(:library, :unknown_key, :location) { 'block value' } + value.should eq('block value') + end + end + + context 'when a block is not provided' do + context 'when the nested object is an array' do + it 'raises an UndefinedPathError' do + lambda do + instance.deep_fetch(:library, :books, 2) + end.should( + raise_error( + DeepFetch::UndefinedPathError, + 'Could not fetch path (library > books > 2) at 2' + ) + ) + end + end + + context 'when the nested object is a hash' do + it 'raises a UndefinedPathError' do + lambda do + instance.deep_fetch(:library, :location, :unknown_key) + end.should( + raise_error( + DeepFetch::UndefinedPathError, + 'Could not fetch path (library > location > unknown_key) at unknown_key' + ) + ) + end + end + end + end + end + end + end +end |