dive 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +3 -0
- data/README.md +94 -0
- data/Rakefile +20 -0
- data/dive.gemspec +22 -0
- data/lib/dive.rb +2 -0
- data/lib/dive/dive.rb +73 -0
- data/lib/dive/noninvasive.rb +2 -0
- data/lib/dive/version.rb +3 -0
- data/spec/invasive/hash_spec.rb +24 -0
- data/spec/noninvasive/read_spec.rb +79 -0
- data/spec/noninvasive/spec_helper.rb +1 -0
- data/spec/noninvasive/write_spec.rb +44 -0
- data/todo.txt +11 -0
- metadata +75 -0
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Dive: for deep hash access
|
|
2
|
+
|
|
3
|
+
Dive is a gem for accessing values within nested Hashes.
|
|
4
|
+
|
|
5
|
+
Why? Well, it all started when I was mapping human readable names taken from a Cucumber table to fields in a JSON response:
|
|
6
|
+
|
|
7
|
+
```cucumber
|
|
8
|
+
Given I have some sausages with
|
|
9
|
+
| Name | Contents |
|
|
10
|
+
| Pork + Fennel | pork mince, fennel, intestine |
|
|
11
|
+
| Soylent Green | people |
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
Given /^I have some sausages with$/ |table|
|
|
16
|
+
fields = {
|
|
17
|
+
'Name' => :name,
|
|
18
|
+
'Contents' => :ingredients
|
|
19
|
+
}
|
|
20
|
+
table.hashes.each do |row|
|
|
21
|
+
sausage = {}
|
|
22
|
+
row.each_pair do |key, value|
|
|
23
|
+
sausage[fields[key]] = value
|
|
24
|
+
end
|
|
25
|
+
@response[:sausages] << sausage
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
@response
|
|
32
|
+
{
|
|
33
|
+
:sausages => [
|
|
34
|
+
{
|
|
35
|
+
:name => 'Pork + Fennel'
|
|
36
|
+
:ingredients => 'pork mince, fennel, intestine'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
:name => 'Soylent Green',
|
|
40
|
+
:ingredients => 'people'
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All fine and dandy until I had to map to values that were nested under the first level of the response:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
@response
|
|
50
|
+
{
|
|
51
|
+
:sausages => [
|
|
52
|
+
{
|
|
53
|
+
:name => 'Pork + Fennel'
|
|
54
|
+
:ingredients => {
|
|
55
|
+
:casing => 'intestine',
|
|
56
|
+
:filling => {
|
|
57
|
+
:meat => 'pork mince',
|
|
58
|
+
:spices => 'fennel'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
I wanted to be able to do something like this:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
fields = {
|
|
70
|
+
'Name' => :sausage_name,
|
|
71
|
+
'Spices' => ':ingredients[:filling[:spices]]'
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
So I did.
|
|
76
|
+
|
|
77
|
+
Check out the specs for how it behaves.
|
|
78
|
+
|
|
79
|
+
### Installation and usage
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
gem install dive
|
|
83
|
+
|
|
84
|
+
require 'dive'
|
|
85
|
+
foods = { :sausages => {:pork_and_fennel => 'DELICIOUS'}}
|
|
86
|
+
foods[':sausages[:pork_and_fennel]']
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or if you become squeamish at the idea of overriding Hash's [] method:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
require 'dive/noninvasive'
|
|
93
|
+
foods.dive ':sausages[:pork_and_fennel]'
|
|
94
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
|
|
4
|
+
task :default => ['spec:invasive', 'spec:noninvasive']
|
|
5
|
+
|
|
6
|
+
namespace :spec do
|
|
7
|
+
rspec_opts = '-f d'
|
|
8
|
+
|
|
9
|
+
desc "Run all examples with invasive Hash extensions"
|
|
10
|
+
RSpec::Core::RakeTask.new('invasive') do |t|
|
|
11
|
+
t.pattern = './spec/invasive/**/*.rb'
|
|
12
|
+
t.rspec_opts = rspec_opts
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Run all examples with non-invasive Hash extensions"
|
|
16
|
+
RSpec::Core::RakeTask.new('noninvasive') do |t|
|
|
17
|
+
t.pattern = './spec/noninvasive/**/*.rb'
|
|
18
|
+
t.rspec_opts = rspec_opts
|
|
19
|
+
end
|
|
20
|
+
end
|
data/dive.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "dive/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "dive"
|
|
7
|
+
s.version = Dive::VERSION
|
|
8
|
+
s.authors = ["Brent Snook"]
|
|
9
|
+
s.email = ["brent@fuglylogic.com"]
|
|
10
|
+
s.homepage = "http://github.com/brentsnook/dive"
|
|
11
|
+
s.summary = %q{A gem for accessing values within nested Hashes}
|
|
12
|
+
s.description = %q{For example: {:sausages => {:pork_and_fennel => 'DELICIOUS'}}[':sausages[:pork_and_fennel]']}
|
|
13
|
+
|
|
14
|
+
s.rubyforge_project = "dive"
|
|
15
|
+
|
|
16
|
+
s.files = `git ls-files`.split("\n")
|
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
19
|
+
s.require_paths = ["lib"]
|
|
20
|
+
|
|
21
|
+
s.add_development_dependency "rspec", "~> 2.7"
|
|
22
|
+
end
|
data/lib/dive.rb
ADDED
data/lib/dive/dive.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'dive/version'
|
|
2
|
+
|
|
3
|
+
module Dive
|
|
4
|
+
|
|
5
|
+
def self.included clazz
|
|
6
|
+
clazz.send :include, Read
|
|
7
|
+
clazz.send :include, Write
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def symbolise key
|
|
11
|
+
is_symbol = key.respond_to?(:to_sym) && key[0] == ':'
|
|
12
|
+
is_symbol ? key[1..-1].to_sym : key
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Read
|
|
16
|
+
|
|
17
|
+
def self.included clazz
|
|
18
|
+
clazz.send :alias_method, :old_read, :[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def dive location
|
|
22
|
+
has_key?(location) ? old_read(location) : dive_read(location)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def dive_read location
|
|
28
|
+
has_key?(symbolise(location)) ? old_read(symbolise(location)) : attempt_dive(location)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def default_value key
|
|
32
|
+
default_proc ? default_proc.call(self, key) : default
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def attempt_dive location
|
|
36
|
+
matches = location.to_s.match /([^\[\]]*)\[(.*)\]/ #(key)[(remainder)]
|
|
37
|
+
matches ? dive_to_next_level(matches[1].strip, matches[2].strip) : default_value(location)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def dive_to_next_level key, remainder
|
|
41
|
+
value = old_read(symbolise(key))
|
|
42
|
+
value.respond_to?(:dive) ? value.dive(remainder) : default_value(remainder)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module Write
|
|
47
|
+
|
|
48
|
+
def self.included clazz
|
|
49
|
+
clazz.send :alias_method, :old_store, :[]=
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dive_store location, value
|
|
53
|
+
matches = location.to_s.match /([^\[\]]*)\[(.*)\]/
|
|
54
|
+
matches ? store_at_next_level(matches[1], matches[2], value): old_store(symbolise(location), value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def store_at_next_level(key, remainder, value)
|
|
60
|
+
storer = has_key?(symbolise(key)) ? old_read(symbolise(key)) : {}
|
|
61
|
+
old_store symbolise(key), storer
|
|
62
|
+
storer.dive_store remainder, value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module Extensions
|
|
68
|
+
def self.included clazz
|
|
69
|
+
clazz.send :alias_method, :[], :dive
|
|
70
|
+
clazz.send :alias_method, :[]=, :dive_store
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/dive/version.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../../lib/dive'
|
|
2
|
+
|
|
3
|
+
describe Hash, 'when extended' do
|
|
4
|
+
|
|
5
|
+
it 'performs a dive on []' do
|
|
6
|
+
hash = {'first' => {'second' => {'third' => 'deep value'}}}
|
|
7
|
+
hash['first[second[third]]'].should == 'deep value'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'performs a dive store on []=' do
|
|
11
|
+
hash = {'first' => {}}
|
|
12
|
+
hash['first[second]'] = 'deep value'
|
|
13
|
+
hash['first']['second'].should == 'deep value'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "allows keys that aren't strings or symbols" do
|
|
17
|
+
{['array'] => 'value'}[['array']].should == 'value'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'allows a nil key' do
|
|
21
|
+
hash = {:x => :y}
|
|
22
|
+
hash[nil].should == hash.default
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Dive, 'when reading' do
|
|
4
|
+
|
|
5
|
+
describe 'with a location that matches a normal key' do
|
|
6
|
+
it 'retrieves the normal value' do
|
|
7
|
+
hash = {'first[second]' => 'normal value', 'first' => {'second' => 'deep value'}}
|
|
8
|
+
hash.dive('first[second]').should == 'normal value'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe 'with a location that matches a deep key' do
|
|
13
|
+
it 'retrieves the deep value' do
|
|
14
|
+
hash = {'first' => {'second' => {'third' => 'deep value'}}}
|
|
15
|
+
hash.dive('first[second[third]]').should == 'deep value'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'retrieves values that have a symbol as key' do
|
|
20
|
+
hash = {:first => {:second => 'deep value'}}
|
|
21
|
+
hash.dive(':first[:second]').should == 'deep value'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'ignores space around location parts' do
|
|
25
|
+
hash = {'first' => {'second' => {'third' => 'deep value'}}}
|
|
26
|
+
hash.dive('first [ second [ third ] ] ').should == 'deep value'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe "when a value at any part of the location doesn't exist" do
|
|
30
|
+
it 'retrieves the default value of the hash where no key was found' do
|
|
31
|
+
hash = {'first' => {'second' => Hash.new('default')}}
|
|
32
|
+
hash.dive('first[second[idontexist]]').should == 'default'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe 'when a hash at any part of the location can not dive' do
|
|
37
|
+
|
|
38
|
+
before do
|
|
39
|
+
@hash = {'first' => {'second' => 'cantdive'}}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'when that hash was created with a default value' do
|
|
43
|
+
it 'retrieves the default value' do
|
|
44
|
+
@hash['first'].default = 'default'
|
|
45
|
+
@hash.dive('first[second[cantdive[fourth]]]').should == 'default'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe 'when that hash was created with a default proc' do
|
|
50
|
+
|
|
51
|
+
before do
|
|
52
|
+
@hash['first'].default_proc = proc { |hash, key| "default proc: #{key}"}
|
|
53
|
+
@result = @hash.dive('first[second[cantdive[fourth]]]')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'retrieves the result of evaluating the default proc' do
|
|
57
|
+
@result.should include('default proc')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'uses the remainder of the location as the proc key' do
|
|
61
|
+
@result.should include('cantdive[fourth]')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'when no normal key or dive location exists' do
|
|
67
|
+
it 'retrieves the default value' do
|
|
68
|
+
hash = Hash.new 'default'
|
|
69
|
+
hash['first'] = 'value'
|
|
70
|
+
hash.dive('idontexist').should == 'default'
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'still attempts to dive when the default value is not nil' do
|
|
75
|
+
hash = Hash.new 'default'
|
|
76
|
+
hash['first'] = {'second' => 'deep value'}
|
|
77
|
+
hash.dive('first[second]').should == 'deep value'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../../lib/dive/noninvasive'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Dive, 'when writing' do
|
|
4
|
+
|
|
5
|
+
describe 'with a deep location' do
|
|
6
|
+
it 'sets a deep value' do
|
|
7
|
+
nested_hash = {'third' => 'deep value'}
|
|
8
|
+
hash = {'first' => {'second' => nested_hash}}
|
|
9
|
+
|
|
10
|
+
hash.dive_store('first[second[third]]', 'deep value')
|
|
11
|
+
|
|
12
|
+
nested_hash['third'].should == 'deep value'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'with a normal location' do
|
|
17
|
+
it 'sets a normal value' do
|
|
18
|
+
hash = {}
|
|
19
|
+
hash.dive_store 'normal', 'normal value'
|
|
20
|
+
hash['normal'].should == 'normal value'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "when parts of the location don't exist" do
|
|
25
|
+
it 'creates each part as a new Hash' do
|
|
26
|
+
hash = {}
|
|
27
|
+
hash.dive_store 'first[second[third]]', 'value'
|
|
28
|
+
hash['first']['second']['third'].should == 'value'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'when the value at the end of a location is not divable' do
|
|
33
|
+
it 'raises a NoMethodError' do
|
|
34
|
+
hash = {'first' => 'not divable'}
|
|
35
|
+
lambda { hash.dive_store 'first[second]', 'value' }.should raise_exception(NoMethodError)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'recognises symbol keys' do
|
|
40
|
+
hash = {:first => {}}
|
|
41
|
+
hash.dive_store ':first[:second]', 'value'
|
|
42
|
+
hash[:first][:second].should == 'value'
|
|
43
|
+
end
|
|
44
|
+
end
|
data/todo.txt
ADDED
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dive
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Brent Snook
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-01-17 00:00:00.000000000Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: rspec
|
|
16
|
+
requirement: &2152401600 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ~>
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '2.7'
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *2152401600
|
|
25
|
+
description: ! 'For example: {:sausages => {:pork_and_fennel => ''DELICIOUS''}}['':sausages[:pork_and_fennel]'']'
|
|
26
|
+
email:
|
|
27
|
+
- brent@fuglylogic.com
|
|
28
|
+
executables: []
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- .gitignore
|
|
33
|
+
- CHANGELOG.md
|
|
34
|
+
- Gemfile
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- dive.gemspec
|
|
38
|
+
- lib/dive.rb
|
|
39
|
+
- lib/dive/dive.rb
|
|
40
|
+
- lib/dive/noninvasive.rb
|
|
41
|
+
- lib/dive/version.rb
|
|
42
|
+
- spec/invasive/hash_spec.rb
|
|
43
|
+
- spec/noninvasive/read_spec.rb
|
|
44
|
+
- spec/noninvasive/spec_helper.rb
|
|
45
|
+
- spec/noninvasive/write_spec.rb
|
|
46
|
+
- todo.txt
|
|
47
|
+
homepage: http://github.com/brentsnook/dive
|
|
48
|
+
licenses: []
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
none: false
|
|
55
|
+
requirements:
|
|
56
|
+
- - ! '>='
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
none: false
|
|
61
|
+
requirements:
|
|
62
|
+
- - ! '>='
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubyforge_project: dive
|
|
67
|
+
rubygems_version: 1.8.10
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 3
|
|
70
|
+
summary: A gem for accessing values within nested Hashes
|
|
71
|
+
test_files:
|
|
72
|
+
- spec/invasive/hash_spec.rb
|
|
73
|
+
- spec/noninvasive/read_spec.rb
|
|
74
|
+
- spec/noninvasive/spec_helper.rb
|
|
75
|
+
- spec/noninvasive/write_spec.rb
|