summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Dooling <tdooling@decisiv.net>2014-02-25 20:45:51 -0600
committerTyler Dooling <tdooling@decisiv.net>2014-04-04 10:28:00 -0500
commit545afab2cdfb852c718e7b0e96b91696470b1c38 (patch)
tree97344d692a06281af60b08c67e501c4e2213432e
parent89d3aac66785e8cb9b17179ea904f79ab34b9888 (diff)
downloadhashie-545afab2cdfb852c718e7b0e96b91696470b1c38.tar.gz
Adds deep_fetch extension for nested access.
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md30
-rw-r--r--lib/hashie.rb1
-rw-r--r--lib/hashie/extensions/deep_fetch.rb29
-rw-r--r--spec/hashie/extensions/deep_fetch_spec.rb70
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).
diff --git a/README.md b/README.md
index b02c741..7f1fd7d 100644
--- a/README.md
+++ b/README.md
@@ -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