key_path 1.0.0
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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +8 -0
- data/LICENSE +22 -0
- data/README.md +85 -0
- data/Rakefile +28 -0
- data/key_path.gemspec +27 -0
- data/lib/key_path/enumerable/extensions.rb +52 -0
- data/lib/key_path/hash/deep_merge.rb +73 -0
- data/lib/key_path/hash/extensions.rb +30 -0
- data/lib/key_path/path.rb +59 -0
- data/lib/key_path/string/extensions.rb +25 -0
- data/lib/key_path/version.rb +3 -0
- data/lib/key_path.rb +14 -0
- data/spec/enumerable_extensions_spec.rb +88 -0
- data/spec/hash_deep_merge_spec.rb +58 -0
- data/spec/hash_extensions_spec.rb +64 -0
- data/spec/key_path_spec.rb +3 -0
- data/spec/path_spec.rb +67 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/string_extensions_spec.rb +41 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5bc45e3830cd4c5a9bbf20765cf382874a890927
|
4
|
+
data.tar.gz: fa82506141b7515dbc9b6f19cbf99f1dffb9f272
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5f1817be54a72fce97109507cb259f1a19392efb7adc6dc4e492ee6c986126c869c6a387ac753b80ef3b604f8051813c19f372592f4c93f6f2827778d8daeeeb
|
7
|
+
data.tar.gz: 4d1aa2f3f87fb7973c6ac2f3296516d70bc38d6ddbbd9436d45a3e4dff60bccceed8c0f84af2e4e88121bdfe64e5998ca2c228bcfa5ac2300aa91c01422a2d31
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Nick Charlton
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# keypath-based collection access extensions for Ruby.
|
2
|
+
|
3
|
+
[](http://travis-ci.org/nickcharlton/keypath-ruby) [](https://codeclimate.com/github/nickcharlton/keypath-ruby) [](https://coveralls.io/r/nickcharlton/keypath-ruby)
|
4
|
+
|
5
|
+
This gem allows you to access nested Ruby collections (`Hash`, `Array`, etc) using
|
6
|
+
keypaths.
|
7
|
+
|
8
|
+
For example, say you had a nested data structure like:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
data = {
|
12
|
+
:item_one => {:id => 1, :url => 'http://nickcharlton.net/'},
|
13
|
+
:something_else => [
|
14
|
+
{
|
15
|
+
:id => 1,
|
16
|
+
:url => 'https://github.com/'
|
17
|
+
}
|
18
|
+
]
|
19
|
+
}
|
20
|
+
```
|
21
|
+
|
22
|
+
You could access "https://github.com/" through: `something_else.0.url`. Basically,
|
23
|
+
this is intended to allow you to manipulate/transform large nested structures that
|
24
|
+
you might get back from a JSON document.
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
Add this line to your application's Gemfile:
|
29
|
+
|
30
|
+
gem 'keypath'
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install keypath
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
`KeyPath` is at least two things. First, it's a class (actually, `KeyPath::Path`)
|
43
|
+
which represents a path (this is just a string, and has methods to go back and
|
44
|
+
forth from it) and secondly a set of class extensions for `Enumerable`, `Hash` and
|
45
|
+
`String` which allow you to use the native collection classes with keypaths.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'key_path'
|
49
|
+
|
50
|
+
data = {
|
51
|
+
:item => {
|
52
|
+
:url => 'http://nickcharlton.net'
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
# fetching a path
|
57
|
+
path = KeyPath::Path.new 'item.url'
|
58
|
+
data.value_at_keypath(path) #=> 'http://nickcharlton.net'
|
59
|
+
|
60
|
+
# finding all `:url` paths in a collection
|
61
|
+
data.keypaths_for_nested_key(:url) #=> {item.url => 'http://nickcharlton.net'}
|
62
|
+
|
63
|
+
# going back and forth from a string
|
64
|
+
path.to_s #=> 'item.url'
|
65
|
+
'item.url'.to_keypath #=> #<KeyPath:70096895112220 path=item.url>
|
66
|
+
|
67
|
+
# get the parent of a keypath (or nil, if there isn't one)
|
68
|
+
path.parent #=> #<KeyPath:70096895221110 path=item>
|
69
|
+
|
70
|
+
# setting a path
|
71
|
+
data.set_keypath(path, 'http://github.com/')
|
72
|
+
```
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create new Pull Request
|
81
|
+
|
82
|
+
## Author
|
83
|
+
|
84
|
+
Copyright (c) 2013 Nick Charlton (<nick@nickcharlton.net>)
|
85
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
##
|
2
|
+
# Initialise Bundler, catch errors.
|
3
|
+
##
|
4
|
+
require 'bundler'
|
5
|
+
require 'bundler/gem_tasks'
|
6
|
+
|
7
|
+
begin
|
8
|
+
Bundler.setup(:default, :development)
|
9
|
+
rescue Bundler::BundlerError => e
|
10
|
+
$stderr.puts e.message
|
11
|
+
$stderr.puts "Run `bundle install` to install missing gems."
|
12
|
+
exit e.status_code
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Configure the test suite.
|
17
|
+
##
|
18
|
+
require 'rake/testtask'
|
19
|
+
|
20
|
+
Rake::TestTask.new :spec do |t|
|
21
|
+
t.test_files = Dir['spec/*_spec.rb']
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# By default, just run the tests.
|
26
|
+
##
|
27
|
+
task :default => :spec
|
28
|
+
|
data/key_path.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'key_path/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'key_path'
|
8
|
+
spec.version = KeyPath::VERSION
|
9
|
+
spec.authors = ['Nick Charlton']
|
10
|
+
spec.email = ['nick@nickcharlton.net']
|
11
|
+
spec.description = %q{Keypath-based collection access extensions for Ruby.}
|
12
|
+
spec.summary = %q{This gem allows you to access nested Ruby collections
|
13
|
+
(Hash, Array, etc) using keypaths. E.g.: 'something.item.0'}
|
14
|
+
spec.homepage = 'https://github.com/nickcharlton/keypath-ruby'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'activesupport', '~> 4.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
25
|
+
spec.add_development_dependency 'rake'
|
26
|
+
spec.add_development_dependency 'pry'
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Enumerable
|
2
|
+
# see: http://stackoverflow.com/a/7139631/83386
|
3
|
+
def value_at_keypath(keypath)
|
4
|
+
if keypath.is_a?(KeyPath::Path)
|
5
|
+
keypath = keypath.to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
parts = keypath.split '.', 2
|
9
|
+
|
10
|
+
# if it's an array, call the index
|
11
|
+
if self[parts[0].to_i]
|
12
|
+
match = self[parts[0].to_i]
|
13
|
+
else
|
14
|
+
match = self[parts[0]] || self[parts[0].to_sym]
|
15
|
+
end
|
16
|
+
|
17
|
+
if !parts[1] or match.nil?
|
18
|
+
return match
|
19
|
+
else
|
20
|
+
return match.value_at_keypath(parts[1])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_keypath(keypath, value)
|
25
|
+
# handle both string and KeyPath::Path forms
|
26
|
+
if keypath.is_a?(String)
|
27
|
+
keypath = keypath.to_keypath
|
28
|
+
end
|
29
|
+
|
30
|
+
# create a collection at the keypath
|
31
|
+
collection = keypath.to_collection
|
32
|
+
|
33
|
+
# set the value in the collection
|
34
|
+
depth = ''
|
35
|
+
keypath.to_a.each do |e|
|
36
|
+
# walk down set and make up the right place to assign
|
37
|
+
if e.is_number?
|
38
|
+
key = "#{e}"
|
39
|
+
else
|
40
|
+
key = ":#{e}"
|
41
|
+
end
|
42
|
+
depth << "[#{key}]"
|
43
|
+
end
|
44
|
+
|
45
|
+
# assign it
|
46
|
+
eval "collection#{depth} = #{value}"
|
47
|
+
|
48
|
+
# merge the new collection into self
|
49
|
+
self.deep_merge!(collection)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#
|
2
|
+
# = Hash Deep Merge
|
3
|
+
#
|
4
|
+
# Merges a Ruby Hash recursively, Also known as deep merge.
|
5
|
+
# Recursive version of Hash#merge and Hash#merge!.
|
6
|
+
#
|
7
|
+
# Category:: Ruby
|
8
|
+
# Package:: Hash
|
9
|
+
# Author:: Simone Carletti <weppos@weppos.net>
|
10
|
+
# Copyright:: 2007-2008 The Authors
|
11
|
+
# License:: MIT License
|
12
|
+
# Link:: http://www.simonecarletti.com/
|
13
|
+
# Source:: https://gist.github.com/weppos/6391
|
14
|
+
#
|
15
|
+
module KeyPath
|
16
|
+
module HashDeepMerge
|
17
|
+
#
|
18
|
+
# Recursive version of Hash#merge!
|
19
|
+
#
|
20
|
+
# Adds the contents of +other_hash+ to +hsh+,
|
21
|
+
# merging entries in +hsh+ with duplicate keys with those from +other_hash+.
|
22
|
+
#
|
23
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
24
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
25
|
+
# it merges and returns the values from both arrays.
|
26
|
+
#
|
27
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
28
|
+
# h2 = {"b" => 254, "c" => 300, "c" => {"c1" => 16, "c3" => 94}}
|
29
|
+
# h1.rmerge!(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
30
|
+
#
|
31
|
+
# Simply using Hash#merge! would return
|
32
|
+
#
|
33
|
+
# h1.merge!(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
34
|
+
#
|
35
|
+
def deep_merge!(other_hash)
|
36
|
+
merge!(other_hash) do |key, oldval, newval|
|
37
|
+
oldval.class == self.class ? oldval.deep_merge!(newval) : newval
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Recursive version of Hash#merge
|
43
|
+
#
|
44
|
+
# Compared with Hash#merge!, this method supports nested hashes.
|
45
|
+
# When both +hsh+ and +other_hash+ contains an entry with the same key,
|
46
|
+
# it merges and returns the values from both arrays.
|
47
|
+
#
|
48
|
+
# Compared with Hash#merge, this method provides a different approch
|
49
|
+
# for merging nasted hashes.
|
50
|
+
# If the value of a given key is an Hash and both +other_hash+ abd +hsh
|
51
|
+
# includes the same key, the value is merged instead replaced with
|
52
|
+
# +other_hash+ value.
|
53
|
+
#
|
54
|
+
# h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
|
55
|
+
# h2 = {"b" => 254, "c" => 300, "c" => {"c1" => 16, "c3" => 94}}
|
56
|
+
# h1.rmerge(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
|
57
|
+
#
|
58
|
+
# Simply using Hash#merge would return
|
59
|
+
#
|
60
|
+
# h1.merge(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
|
61
|
+
#
|
62
|
+
def deep_merge(other_hash)
|
63
|
+
r = {}
|
64
|
+
merge(other_hash) do |key, oldval, newval|
|
65
|
+
r[key] = oldval.class == self.class ? oldval.deep_merge(newval) : newval
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Hash
|
72
|
+
include KeyPath::HashDeepMerge
|
73
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module KeyPath
|
2
|
+
module HashExtensions
|
3
|
+
def keypaths_for_nested_key(nested_key = '', nested_hash=self, path=[], all_values={})
|
4
|
+
nested_hash.each do |k, v|
|
5
|
+
path << k.to_s # assemble the path from the key
|
6
|
+
case v
|
7
|
+
when Array then
|
8
|
+
v.each_with_index do |item, i|
|
9
|
+
path << "#{i}" # add the array key
|
10
|
+
keypaths_for_nested_key(nested_key, item, path, all_values)
|
11
|
+
end
|
12
|
+
path.pop # remove the array key
|
13
|
+
when Hash then keypaths_for_nested_key(nested_key, v, path, all_values)
|
14
|
+
else
|
15
|
+
if k == nested_key
|
16
|
+
all_values.merge!({"#{path.join('.')}" => "#{v}"})
|
17
|
+
end
|
18
|
+
path.pop
|
19
|
+
end
|
20
|
+
end
|
21
|
+
path.pop
|
22
|
+
|
23
|
+
return all_values
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Hash
|
29
|
+
include KeyPath::HashExtensions
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module KeyPath
|
2
|
+
class Path
|
3
|
+
def initialize(path='')
|
4
|
+
@path = path
|
5
|
+
end
|
6
|
+
|
7
|
+
def parent
|
8
|
+
s = self.to_a
|
9
|
+
s.pop
|
10
|
+
|
11
|
+
# there's no parent if it's empty
|
12
|
+
if s == []
|
13
|
+
return nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# otherwise, join them back together and pass back a path
|
17
|
+
s.join('.').to_keypath
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@path
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_a
|
25
|
+
@path.split('.')
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_collection
|
29
|
+
collection = {}
|
30
|
+
s = self.to_a
|
31
|
+
depth = ''
|
32
|
+
|
33
|
+
s.each_with_index do |e, i|
|
34
|
+
# assemble the key
|
35
|
+
if e.is_number?
|
36
|
+
key = "#{e}"
|
37
|
+
else
|
38
|
+
key = ":#{e}"
|
39
|
+
end
|
40
|
+
depth << "[#{key}]"
|
41
|
+
|
42
|
+
# figure out the correct type to push
|
43
|
+
type = {}
|
44
|
+
if e.is_plural?
|
45
|
+
type = []
|
46
|
+
end
|
47
|
+
|
48
|
+
# evaluate this stage
|
49
|
+
eval "collection#{depth} = #{type}"
|
50
|
+
end
|
51
|
+
|
52
|
+
collection
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class.name}:#{self.object_id} path=#{@path}>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module KeyPath
|
4
|
+
module StringExtensions
|
5
|
+
def to_keypath
|
6
|
+
KeyPath::Path.new self
|
7
|
+
end
|
8
|
+
|
9
|
+
def is_singular?
|
10
|
+
self.pluralize != self and self.singularize == self
|
11
|
+
end
|
12
|
+
|
13
|
+
def is_plural?
|
14
|
+
self.singularize != self and self.pluralize == self
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_number?
|
18
|
+
true if Float(self) rescue false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class String
|
24
|
+
include KeyPath::StringExtensions
|
25
|
+
end
|
data/lib/key_path.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# gem bits
|
2
|
+
require 'key_path/version'
|
3
|
+
require 'key_path/path'
|
4
|
+
|
5
|
+
# extensions
|
6
|
+
require 'key_path/hash/deep_merge'
|
7
|
+
require 'key_path/hash/extensions'
|
8
|
+
require 'key_path/enumerable/extensions'
|
9
|
+
require 'key_path/string/extensions'
|
10
|
+
|
11
|
+
|
12
|
+
module KeyPath
|
13
|
+
# Your code goes here...
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# test helpers
|
2
|
+
require File.expand_path 'spec_helper.rb', __dir__
|
3
|
+
|
4
|
+
describe 'EnumerableExtensions' do
|
5
|
+
it 'adds methods to collections' do
|
6
|
+
hash = {:id => 1}
|
7
|
+
|
8
|
+
hash.must_respond_to 'value_at_keypath'
|
9
|
+
hash.must_respond_to 'set_keypath'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'can fetch simple path values' do
|
13
|
+
hash = {:id => 1}
|
14
|
+
|
15
|
+
hash.value_at_keypath('id').must_equal 1
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'can accept KeyPath::Path objects for keypaths' do
|
19
|
+
hash = {:id => 1}
|
20
|
+
keypath = KeyPath::Path.new('id')
|
21
|
+
|
22
|
+
hash.value_at_keypath(keypath).must_equal 1
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
it 'can fetch with a nested hash key path' do
|
27
|
+
hash = {:item => {:id => 1}}
|
28
|
+
|
29
|
+
hash.value_at_keypath('item.id').must_equal 1
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'can fetch a nested array object' do
|
33
|
+
hash = {:items => [{:id => 1}]}
|
34
|
+
|
35
|
+
hash.value_at_keypath('items.0').must_equal({:id => 1})
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'can set a simple value using a keypath string' do
|
39
|
+
source = {:item => {:id => {}}}
|
40
|
+
keypath = 'item.id'
|
41
|
+
value = 1
|
42
|
+
|
43
|
+
source.set_keypath(keypath, value)
|
44
|
+
|
45
|
+
source.value_at_keypath(keypath).must_equal(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can set a simple value using a KeyPath::Path object' do
|
49
|
+
source = {:item => {:id => {}}}
|
50
|
+
keypath = KeyPath::Path.new('item.id')
|
51
|
+
value = 1
|
52
|
+
|
53
|
+
source.set_keypath(keypath, value)
|
54
|
+
|
55
|
+
source.value_at_keypath(keypath).must_equal(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'can set a string value using a KeyPath::Path object' do
|
59
|
+
source = {:item => {:id => {}}}
|
60
|
+
keypath = KeyPath::Path.new('item.id')
|
61
|
+
value = 'value'
|
62
|
+
|
63
|
+
source.set_keypath(keypath, value)
|
64
|
+
|
65
|
+
source.value_at_keypath(keypath).must_equal(value)
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
it 'can set a hash for a path' do
|
70
|
+
source = {:item => {:id => {}}}
|
71
|
+
keypath = KeyPath::Path.new('item')
|
72
|
+
value = {:id => 1}
|
73
|
+
|
74
|
+
source.set_keypath(keypath, value)
|
75
|
+
|
76
|
+
source.value_at_keypath(keypath).must_equal(value)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'can set a value in a nested array' do
|
80
|
+
source = {:items => [{:id => 1}]}
|
81
|
+
keypath = KeyPath::Path.new('items.1')
|
82
|
+
value = {:id => 2}
|
83
|
+
|
84
|
+
source.set_keypath(keypath, value)
|
85
|
+
|
86
|
+
source.value_at_keypath(keypath).must_equal(value)
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# test helpers
|
2
|
+
require File.expand_path 'spec_helper.rb', __dir__
|
3
|
+
|
4
|
+
describe 'HashDeepMerge' do
|
5
|
+
it 'adds methods to the Hash class' do
|
6
|
+
hash = {:id => 1}
|
7
|
+
|
8
|
+
hash.must_respond_to 'deep_merge'
|
9
|
+
hash.must_respond_to 'deep_merge!'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'recursively adds two hashes' do
|
13
|
+
one = {:one => {:id => 1}}
|
14
|
+
two = {:one => {:url => 'http://nickcharlton.net'}}
|
15
|
+
three = {:one => {:id => 1, :url => 'http://nickcharlton.net'}}
|
16
|
+
|
17
|
+
output = one.deep_merge(two)
|
18
|
+
|
19
|
+
output.must_be_kind_of Hash
|
20
|
+
output.must_equal three
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'recursively adds two hashes in place' do
|
24
|
+
one = {:one => {:id => 1}}
|
25
|
+
two = {:one => {:url => 'http://nickcharlton.net'}}
|
26
|
+
three = {:one => {:id => 1, :url => 'http://nickcharlton.net'}}
|
27
|
+
|
28
|
+
one.deep_merge!(two)
|
29
|
+
|
30
|
+
one.must_be_kind_of Hash
|
31
|
+
one.must_equal three
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'HashNormalMerge' do
|
36
|
+
it 'combines nested hashes without decending into them' do
|
37
|
+
one = {'a' => 100, 'b' => 200, 'c' => {'c1' => 12, 'c2' => 14}}
|
38
|
+
two = {'b' => 254, 'c' => 300, 'c' => {'c1' => 16, 'c3' => 94}}
|
39
|
+
expected = {'a' => 100, 'b' => 254, 'c' => {'c1' => 16, 'c3' => 94}}
|
40
|
+
|
41
|
+
output = one.merge(two)
|
42
|
+
|
43
|
+
output.must_be_kind_of Hash
|
44
|
+
output.must_equal expected
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'combines nested hashes without decending into them in place' do
|
48
|
+
one = {'a' => 100, 'b' => 200, 'c' => {'c1' => 12, 'c2' => 14}}
|
49
|
+
two = {'b' => 254, 'c' => 300, 'c' => {'c1' => 16, 'c3' => 94}}
|
50
|
+
expected = {'a' => 100, 'b' => 254, 'c' => {'c1' => 16, 'c3' => 94}}
|
51
|
+
|
52
|
+
one.merge!(two)
|
53
|
+
|
54
|
+
one.must_be_kind_of Hash
|
55
|
+
one.must_equal expected
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# test helpers
|
2
|
+
require File.expand_path 'spec_helper.rb', __dir__
|
3
|
+
|
4
|
+
describe 'HashExtensions' do
|
5
|
+
it 'adds methods to the Hash class' do
|
6
|
+
hash = {:id => 1}
|
7
|
+
|
8
|
+
hash.must_respond_to 'keypaths_for_nested_key'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'can find keys in a nested hash' do
|
12
|
+
data = {
|
13
|
+
:id => 1,
|
14
|
+
:item => {
|
15
|
+
:id => 2,
|
16
|
+
:name => 'an item'
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
keypaths = data.keypaths_for_nested_key(:id)
|
21
|
+
|
22
|
+
keypaths.wont_be_nil
|
23
|
+
keypaths.must_include 'id'
|
24
|
+
keypaths.must_include 'item.id'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can find keys in nested array' do
|
28
|
+
data = {
|
29
|
+
:id => 1,
|
30
|
+
:items => [{
|
31
|
+
:id => 2,
|
32
|
+
:name => 'an item'
|
33
|
+
},
|
34
|
+
{
|
35
|
+
:id => 3,
|
36
|
+
:name => 'another item'
|
37
|
+
}]
|
38
|
+
}
|
39
|
+
|
40
|
+
keypaths = data.keypaths_for_nested_key(:id)
|
41
|
+
|
42
|
+
keypaths.wont_be_nil
|
43
|
+
keypaths.must_include 'id'
|
44
|
+
keypaths.must_include 'items.0.id'
|
45
|
+
keypaths.must_include 'items.1.id'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'can handle walking into an otherwise unknown object' do
|
49
|
+
class ExampleClass
|
50
|
+
def initialize
|
51
|
+
@name = 'Example Class'
|
52
|
+
end
|
53
|
+
|
54
|
+
example = ExampleClass.new
|
55
|
+
|
56
|
+
data = {:items => example}
|
57
|
+
|
58
|
+
keypaths = data.keypaths_for_nested_key(:items)
|
59
|
+
|
60
|
+
keypaths.wont_be_nil
|
61
|
+
keypaths.must_include 'items'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/spec/path_spec.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# test helpers
|
2
|
+
require File.expand_path 'spec_helper.rb', __dir__
|
3
|
+
|
4
|
+
describe 'KeyPath::Path main methods' do
|
5
|
+
it 'creates a KeyPath instance from a string' do
|
6
|
+
path = KeyPath::Path.new('item.url')
|
7
|
+
|
8
|
+
path.wont_be_nil
|
9
|
+
path.to_s.must_be_kind_of String
|
10
|
+
path.to_s.must_equal 'item.url'
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'outputs an array of items in the path' do
|
14
|
+
path = KeyPath::Path.new('item.url')
|
15
|
+
|
16
|
+
path.to_a.must_be_kind_of Array
|
17
|
+
path.to_a.must_equal ['item', 'url']
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'spits out a useful inspect string' do
|
21
|
+
path = KeyPath::Path.new('item.url')
|
22
|
+
|
23
|
+
path.inspect.must_be_kind_of String
|
24
|
+
path.inspect.wont_be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns a parent if one exists' do
|
28
|
+
path = KeyPath::Path.new('item.url')
|
29
|
+
|
30
|
+
path.parent.wont_be_nil
|
31
|
+
path.parent.must_be_kind_of KeyPath::Path
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns nil if a parent doesnt exist' do
|
35
|
+
path = KeyPath::Path.new('item')
|
36
|
+
|
37
|
+
path.parent.must_be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'KeyPath::Path collections generation' do
|
42
|
+
it 'returns an empty hash with an empty path' do
|
43
|
+
path = KeyPath::Path.new('')
|
44
|
+
path.to_collection.must_equal({})
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns a nested hash for a single path unit' do
|
48
|
+
path = KeyPath::Path.new('item')
|
49
|
+
path.to_collection.must_equal({:item => {}})
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns a nested array when the key is plural' do
|
53
|
+
path = KeyPath::Path.new('items')
|
54
|
+
path.to_collection.must_equal({:items => []})
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'returns a double nested array with a two set keypath' do
|
58
|
+
path = KeyPath::Path.new('item.id')
|
59
|
+
path.to_collection.must_equal({:item => {:id => {}}})
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns a nested array with an item' do
|
63
|
+
path = KeyPath::Path.new('items.0.id')
|
64
|
+
path.to_collection.must_equal({:items => [{:id => {}}]})
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# test helpers
|
2
|
+
require File.expand_path 'spec_helper.rb', __dir__
|
3
|
+
|
4
|
+
describe 'StringExtensions' do
|
5
|
+
it 'can create a KeyPath instance from itself' do
|
6
|
+
example_string = 'item.url'
|
7
|
+
|
8
|
+
example_string.to_keypath.wont_be_nil
|
9
|
+
example_string.to_keypath.must_be_kind_of KeyPath::Path
|
10
|
+
|
11
|
+
# around and around we go
|
12
|
+
example_string.to_keypath.to_s.must_equal example_string
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can make a set of strings plural' do
|
16
|
+
%w(word rail dress business).each do |v|
|
17
|
+
v.is_plural?.must_equal false
|
18
|
+
end
|
19
|
+
|
20
|
+
%w(words rails dresses businesses).each do |v|
|
21
|
+
v.is_plural?.must_equal true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can make a set of string singular' do
|
26
|
+
%w(word rail dress business).each do |v|
|
27
|
+
v.is_singular?.must_equal true
|
28
|
+
end
|
29
|
+
|
30
|
+
%w(words rails dresses businesses).each do |v|
|
31
|
+
v.is_singular?.must_equal false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can test if a string is actually a number' do
|
36
|
+
'0'.is_number?.must_equal true
|
37
|
+
'1234567890'.is_number?.must_equal true
|
38
|
+
|
39
|
+
'item'.is_number?.must_equal false
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: key_path
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Charlton
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Keypath-based collection access extensions for Ruby.
|
70
|
+
email:
|
71
|
+
- nick@nickcharlton.net
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .coveralls.yml
|
77
|
+
- .gitignore
|
78
|
+
- .travis.yml
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- key_path.gemspec
|
84
|
+
- lib/key_path.rb
|
85
|
+
- lib/key_path/enumerable/extensions.rb
|
86
|
+
- lib/key_path/hash/deep_merge.rb
|
87
|
+
- lib/key_path/hash/extensions.rb
|
88
|
+
- lib/key_path/path.rb
|
89
|
+
- lib/key_path/string/extensions.rb
|
90
|
+
- lib/key_path/version.rb
|
91
|
+
- spec/enumerable_extensions_spec.rb
|
92
|
+
- spec/hash_deep_merge_spec.rb
|
93
|
+
- spec/hash_extensions_spec.rb
|
94
|
+
- spec/key_path_spec.rb
|
95
|
+
- spec/path_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
- spec/string_extensions_spec.rb
|
98
|
+
homepage: https://github.com/nickcharlton/keypath-ruby
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.0.14
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: 'This gem allows you to access nested Ruby collections (Hash, Array, etc)
|
122
|
+
using keypaths. E.g.: ''something.item.0'''
|
123
|
+
test_files:
|
124
|
+
- spec/enumerable_extensions_spec.rb
|
125
|
+
- spec/hash_deep_merge_spec.rb
|
126
|
+
- spec/hash_extensions_spec.rb
|
127
|
+
- spec/key_path_spec.rb
|
128
|
+
- spec/path_spec.rb
|
129
|
+
- spec/spec_helper.rb
|
130
|
+
- spec/string_extensions_spec.rb
|
131
|
+
has_rdoc:
|