sparsify 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.
- data/.gitignore +17 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +13 -0
- data/README.md +55 -0
- data/Rakefile +9 -0
- data/git_hooks/pre-commit/ruby-appraiser +17 -0
- data/lib/sparsify.rb +96 -0
- data/lib/sparsify/guard_methods.rb +24 -0
- data/lib/sparsify/helper_methods.rb +17 -0
- data/lib/sparsify/utility_methods.rb +178 -0
- data/lib/sparsify/version.rb +4 -0
- data/sparsify.gemspec +25 -0
- data/spec/sparsify_spec.rb +197 -0
- metadata +126 -0
data/.gitignore
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
`Sparsify` is [Apache-liennsed](LICENSE.txt).
|
4
|
+
|
5
|
+
## Git-Flow
|
6
|
+
|
7
|
+
`Sparsify` follows the [git-flow][] branching model, which means that every
|
8
|
+
commit on `master` is a release. The default working branch is `develop`, so
|
9
|
+
in general please keep feature pull-requests based against the current
|
10
|
+
`develop`.
|
11
|
+
|
12
|
+
- fork sparsify
|
13
|
+
- use the git-flow model to start your feature or hotfix
|
14
|
+
- make some commits (please include specs)
|
15
|
+
- submit a pull-request
|
16
|
+
|
17
|
+
## Bug Reporting
|
18
|
+
|
19
|
+
Please include clear steps-to-reproduce. Spec files are especially welcome;
|
20
|
+
a failing spec can be contributed as a pull-request against `develop`.
|
21
|
+
|
22
|
+
## Documentation
|
23
|
+
|
24
|
+
`Sparsify` uses YARDOC, and so must your pull-requests.
|
25
|
+
|
26
|
+
## Ruby Appraiser
|
27
|
+
|
28
|
+
`Sparsify` uses the [ruby-appraiser][] gem via [pre-commit][] hook, which can be
|
29
|
+
activated by installing [icefox/git-hooks][] and running `git-hooks --install`.
|
30
|
+
Reek and Rubocop are strong guidelines; use them to reduce defects as much as
|
31
|
+
you can, but if you believe clarity will be sacrificed they can be bypassed
|
32
|
+
with the `--no-verify` flag.
|
33
|
+
|
34
|
+
[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/
|
35
|
+
[pre-commit]: .githooks/pre-commit/ruby-appraiser
|
36
|
+
[ruby-appraiser]: https://github.com/simplymeasured/ruby-appraiser
|
37
|
+
[icefox/git-hooks]: https://github.com/icefox/git-hooks
|
38
|
+
[pull-request-hack]: http://felixge.de/2013/03/11/the-pull-request-hack.html
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Simply Measured
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Sparsify
|
2
|
+
|
3
|
+
Convert a deeply-nested hash into a shallow sparse hash. Useful for tools that
|
4
|
+
either can't handle deeply-nested hashes or that allow partial updates via
|
5
|
+
sparse hashes.
|
6
|
+
|
7
|
+
This gem is our internal implementation of our [Sparisfy Challenge][].
|
8
|
+
|
9
|
+
[Sparsify Challenge]: https://github.com/simplymeasured/sparsify-challenge
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require 'sparsify'
|
15
|
+
|
16
|
+
Sparsify({'foo' => { 'bar' => {'baz' => 'bingo'}}})
|
17
|
+
#=> {'foo.bar.baz' => 'bingo'}
|
18
|
+
|
19
|
+
Unsparsify({'foo.bar.baz' => 'bingo'})
|
20
|
+
#=> {'foo' => { 'bar' => {'baz' => 'bingo'}}}
|
21
|
+
```
|
22
|
+
|
23
|
+
## Advanced Usage
|
24
|
+
|
25
|
+
### Custom Separator
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'sparsify'
|
29
|
+
|
30
|
+
Sparsify({'foo' => { 'bar.bar' => {'baz' => 'bingo'}}}, separator: '|')
|
31
|
+
#=> {'foo|bar.bar|baz' => 'bingo'}
|
32
|
+
|
33
|
+
Unsparsify({'foo|bar.bar|baz' => 'bingo'}, separator: '|')
|
34
|
+
#=> {'foo' => { 'bar.bar' => {'baz' => 'bingo'}}}
|
35
|
+
```
|
36
|
+
|
37
|
+
### Sparse Arrays
|
38
|
+
|
39
|
+
``` ruby
|
40
|
+
require 'sparsify'
|
41
|
+
|
42
|
+
Sparsify({'foo' => ['bar','baz','buz']}, sparse_array: true)
|
43
|
+
#=> {'foo.0'=>'bar','foo.1'=>'baz','foo.2'=>'buz'}
|
44
|
+
|
45
|
+
Unsparsify({'foo.0'=>'bar','foo.1'=>'baz','foo.2'=>'buz'}, sparse_array: true)
|
46
|
+
#=> {'foo' => ['bar','baz','buz']}
|
47
|
+
```
|
48
|
+
|
49
|
+
## Contributing
|
50
|
+
|
51
|
+
1. Fork it
|
52
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
53
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
54
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
55
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
echo -e "\033[0;36mRuby Appraiser: running\033[0m"
|
3
|
+
bundle exec ruby-appraiser --mode=authored rubocop --verbose
|
4
|
+
result_code=$?
|
5
|
+
if [ $result_code -gt "0" ]; then
|
6
|
+
echo -en "\033[0;31m" # RED
|
7
|
+
echo "[✘] Ruby Appraiser found newly-created defects and "
|
8
|
+
echo " has blocked your commit."
|
9
|
+
echo " Fix the defects and commit again."
|
10
|
+
echo " To bypass, commit again with --no-verify."
|
11
|
+
echo -en "\033[0m" # RESET
|
12
|
+
exit $result_code
|
13
|
+
else
|
14
|
+
echo -en "\033[0;32m" # GREEN
|
15
|
+
echo "[✔] Ruby Appraiser ok"
|
16
|
+
echo -en "\033[0m" #RESET
|
17
|
+
fi
|
data/lib/sparsify.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'sparsify/version'
|
4
|
+
require 'sparsify/utility_methods'
|
5
|
+
require 'sparsify/helper_methods'
|
6
|
+
require 'sparsify/guard_methods'
|
7
|
+
|
8
|
+
# Provides sparse-key access to a Hash.
|
9
|
+
#
|
10
|
+
# {'foo'=>{'bar'=>'bingo'}}.sparse #=> {'foo.bar'=>'bingo'}
|
11
|
+
# {'foo.bar'=>'bingo'}.unsparse => {'foo'=>{'bar'=>'bingo'}}
|
12
|
+
#
|
13
|
+
module Sparsify
|
14
|
+
# The default separator, used if not specified in command's
|
15
|
+
# options hash.
|
16
|
+
DEFAULT_SEPARATOR = '.'.freeze
|
17
|
+
|
18
|
+
# Returns a sparse version of self using the options provided.
|
19
|
+
#
|
20
|
+
# @param options [Hash<Symbol,Object>]
|
21
|
+
# @option options [String] :separator
|
22
|
+
# @option options [String] :prefix
|
23
|
+
# @option options [Boolean,:zero_pad] :sparse_array (false)
|
24
|
+
# truthy values will cause arrays to be sparsed by index and decended into.
|
25
|
+
# :zero_pad causes indexes to be zero-padded to make them sort lexically.
|
26
|
+
# @return [Hash<String,Object>]
|
27
|
+
def sparse(options = {})
|
28
|
+
Sparsify.sparse(self, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Replaces self with sparse version of itself.
|
32
|
+
#
|
33
|
+
# @param options (see #sparse)
|
34
|
+
# @return [Hash<String,Object>]
|
35
|
+
def sparse!(options = {})
|
36
|
+
self.replace(sparse, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Used internally by both Sparsify::Utility#sparse and
|
40
|
+
# Sparsify::Utility#unsparse
|
41
|
+
#
|
42
|
+
# @overload sparse_eachrm (options = {}, &block)
|
43
|
+
# Yields once per key in sparse version of itself.
|
44
|
+
# @param options (see #sparse)
|
45
|
+
# @yieldparam [(sparse_key,value)]
|
46
|
+
# @return [void]
|
47
|
+
# @overload sparse_each(options = {})
|
48
|
+
# @param options (see #sparse)
|
49
|
+
# @return [Enumerator<(sparse_key,value)>]
|
50
|
+
def sparse_each(options = {}, &block)
|
51
|
+
Sparsify.sparse_each(self, options, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Follows semantics of Hash#fetch
|
55
|
+
#
|
56
|
+
# @overload sparse_fetch(sparse_key, options = {})
|
57
|
+
# @param options (see #sparse)
|
58
|
+
# @raise [KeyError] if sparse_key not found
|
59
|
+
# @return [Object]
|
60
|
+
# @overload sparse_fetch(sparse_key, default, options = {})
|
61
|
+
# @param options (see #sparse)
|
62
|
+
# @param default [Object] the default object
|
63
|
+
# @return [default]
|
64
|
+
# @overload sparse_fetch(sparse_key, options = {}, &block)
|
65
|
+
# @param options (see #sparse)
|
66
|
+
# @yield if sparse_key not founs
|
67
|
+
# @return [Object] that which was returned by the given block.
|
68
|
+
def sparse_fetch(*args, &block)
|
69
|
+
Sparsify.sparse_fetch(self, *args, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Follows semantics of Hash#[] without support for Hash#default_proc
|
73
|
+
#
|
74
|
+
# @overload sparse_get(sparse_key, options = {})
|
75
|
+
# @param options (see #sparse)
|
76
|
+
# @return [Object] at that address or nil if none found
|
77
|
+
def sparse_fetch(*args, &block)
|
78
|
+
Sparsify.sparse_fetch(self, *args, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns a deeply-nested hash version of self.
|
82
|
+
#
|
83
|
+
# @param options (see #sparse)
|
84
|
+
# @return [Hash<String,Object>]
|
85
|
+
def unsparse(options = {})
|
86
|
+
Sparsify.unsparse(self, options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Replaces self with deeply-nested version of self.
|
90
|
+
#
|
91
|
+
# @param options (see #sparse)
|
92
|
+
# @return [Hash<String,Object>]
|
93
|
+
def unsparse!(options = {})
|
94
|
+
self.replace(unsparse, options)
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Sparsify
|
4
|
+
# Methods to ensure Sparsify isn't mixed into nonsensical things
|
5
|
+
module GuardMethods
|
6
|
+
# Sparsify can be *extended* into instances of Hash
|
7
|
+
# @param base [Hash]
|
8
|
+
def extended(base)
|
9
|
+
unless base.is_a? Hash
|
10
|
+
raise ArgumentError, "<#{base.inspect}> is not a Hash!"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Sparsigy can be *included* into implementations of Hash
|
15
|
+
# @param base [Hash.class]
|
16
|
+
def included(base)
|
17
|
+
unless base <= Hash
|
18
|
+
raise ArgumentError, "<#{base.inspect} does not inherit Hash"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend GuardMethods
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Sparsify
|
4
|
+
module HelperMethods
|
5
|
+
def Sparsify(hsh, options = {})
|
6
|
+
Sparsify.sparse(hsh, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def Unsparsify(hsh, options = {})
|
10
|
+
Sparsify.unsparse(hsh, options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
extend HelperMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
include Sparsify::HelperMethods
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Sparsify
|
4
|
+
# The Utility Methods provide a significant (~4x) performance increase
|
5
|
+
# over extend-ing instance methods everywhere we need them.
|
6
|
+
module UtilityMethods
|
7
|
+
|
8
|
+
# Provides a way to iterate through a deeply-nested hash as if it were
|
9
|
+
# a sparse-hash. Used internally for generating and deconstructing sparse
|
10
|
+
# hashes.
|
11
|
+
#
|
12
|
+
# @overload sparse_each(hsh, options = {}, &block)
|
13
|
+
# Yields once per key in sparse version of itself.
|
14
|
+
# @param hsh [Hash<#to_s,Object>]
|
15
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
16
|
+
# @yieldparam [(sparse_key,value)]
|
17
|
+
# @return [void]
|
18
|
+
# @overload sparse_each(hsh, options = {})
|
19
|
+
# @param hsh [Hash<#to_s,Object>]
|
20
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
21
|
+
# @return [Enumerator<(sparse_key,value)>]
|
22
|
+
def sparse_each(hsh, options = {}, &block)
|
23
|
+
return enum_for(:sparse_each, hsh, options) unless block_given?
|
24
|
+
|
25
|
+
inherited_prefix = options.fetch(:prefix, nil)
|
26
|
+
separator = options.fetch(:separator, DEFAULT_SEPARATOR)
|
27
|
+
sparse_array = options.fetch(:sparse_array, false)
|
28
|
+
|
29
|
+
hsh.each do |partial_key, value|
|
30
|
+
key = escaped_join(inherited_prefix, partial_key.to_s, separator)
|
31
|
+
if value.kind_of?(Hash) && !value.empty?
|
32
|
+
sparse_each(value, options.merge(prefix: key), &block)
|
33
|
+
elsif sparse_array && value.kind_of?(Array) && !value.empty?
|
34
|
+
zps = (sparse_array == :zero_pad ? "%0#{value.count.to_s.size}d" : '%d')# zero-pad string
|
35
|
+
sparse_each(value.count.times.map(&zps.method(:%)).zip(value), options.merge(prefix: key), &block)
|
36
|
+
else
|
37
|
+
yield key, value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a sparse version of the given hash
|
43
|
+
#
|
44
|
+
# @param hsh [Hash<#to_s,Object>]
|
45
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
46
|
+
# @return [Hash<String,Object>]
|
47
|
+
def sparse(hsh, options = {})
|
48
|
+
enum = sparse_each(hsh, options)
|
49
|
+
enum.each_with_object(Hash.new) do |(key, value), memo|
|
50
|
+
memo[key] = value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a deeply-nested version of the given sparse hash
|
55
|
+
# @param hsh [Hash<#to_s,Object>]
|
56
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
57
|
+
# @return [Hash<String,Object>]
|
58
|
+
def unsparse(hsh, options = {})
|
59
|
+
separator = options.fetch(:separator, DEFAULT_SEPARATOR)
|
60
|
+
hsh.each_with_object({}) do |(k, v), memo|
|
61
|
+
current = memo
|
62
|
+
key = escaped_split(k, separator)
|
63
|
+
up_next = partial = key.shift
|
64
|
+
until key.size.zero?
|
65
|
+
up_next = key.shift
|
66
|
+
up_next = up_next.to_i if (up_next =~ /\A[0-9]+\Z/)
|
67
|
+
current = (current[partial] ||= (up_next.kind_of?(Integer) ? [] : {}))
|
68
|
+
case up_next
|
69
|
+
when Integer then raise KeyError unless current.kind_of?(Array)
|
70
|
+
else raise KeyError unless current.kind_of?(Hash)
|
71
|
+
end
|
72
|
+
partial = up_next
|
73
|
+
end
|
74
|
+
current[up_next] = v
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Fetch a sparse key from the given deeply-nested hash.
|
79
|
+
#
|
80
|
+
# @overload sparse_fetch(hsh, sparse_key, default, options = {})
|
81
|
+
# @param hsh [Hash<#to_s,Object>]
|
82
|
+
# @param sparse_key [#to_s]
|
83
|
+
# @param default [Object] returned if sparse key not found
|
84
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
85
|
+
# @return [Object]
|
86
|
+
# @overload sparse_fetch(hsh, sparse_key, options = {}, &block)
|
87
|
+
# @param hsh [Hash<#to_s,Object>]
|
88
|
+
# @param sparse_key [#to_s]
|
89
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
90
|
+
# @yieldreturn is returned if key not found
|
91
|
+
# @return [Object]
|
92
|
+
# @overload sparse_fetch(hsh, sparse_key, options = {})
|
93
|
+
# @param hsh [Hash<#to_s,Object>]
|
94
|
+
# @param sparse_key [#to_s]
|
95
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
96
|
+
# @raise KeyError if key not found
|
97
|
+
# @return [Object]
|
98
|
+
def sparse_fetch(hsh, sparse_key, *args, &block)
|
99
|
+
options = ( args.last.kind_of?(Hash) ? args.pop : {})
|
100
|
+
default = args.pop
|
101
|
+
|
102
|
+
separator = options.fetch(:separator, DEFAULT_SEPARATOR)
|
103
|
+
|
104
|
+
escaped_split(sparse_key, separator).reduce(hsh) do |memo, kp|
|
105
|
+
if memo.kind_of?(Hash) and memo.has_key?(kp)
|
106
|
+
memo.fetch(kp)
|
107
|
+
elsif default
|
108
|
+
return default
|
109
|
+
elsif block_given?
|
110
|
+
return yield
|
111
|
+
else
|
112
|
+
raise KeyError, sparse_key
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Get a sparse key from the given deeply-nested hash, or return nil
|
118
|
+
# if key not found.
|
119
|
+
#
|
120
|
+
# Worth noting is that Hash#default_proc is *not* used, as the intricacies
|
121
|
+
# of implementation would lead to all sorts of terrible surprises.
|
122
|
+
#
|
123
|
+
# @param hsh [Hash<#to_s,Object>]
|
124
|
+
# @param sparse_key [#to_s]
|
125
|
+
# @param options (see Sparsify::UtilityMethods#sparse)
|
126
|
+
# @return [Object]
|
127
|
+
def sparse_get(hsh, sparse_key, options = {})
|
128
|
+
sparse_fetch(hsh, sparse_key, options) { nil }
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Utility method for backslash-escaping a string
|
134
|
+
# @param str [String]
|
135
|
+
# @param separator [String] single-character string
|
136
|
+
# @return [String]
|
137
|
+
def escape(str, separator)
|
138
|
+
pattern = /(\\|#{Regexp.escape(separator)})/
|
139
|
+
str.gsub(pattern, '\\\\\1')
|
140
|
+
end
|
141
|
+
|
142
|
+
# Utility method for removing backslash-escaping from a string
|
143
|
+
# @param str [String]
|
144
|
+
# @param separator [String] single-character string
|
145
|
+
# @return [String]
|
146
|
+
def unescape(str, separator)
|
147
|
+
pattern = /\\(\\|#{Regexp.escape(separator)})/
|
148
|
+
str.gsub(pattern, '\1')
|
149
|
+
end
|
150
|
+
|
151
|
+
# Utility method for splitting a string by a separator into
|
152
|
+
# non-escaped parts
|
153
|
+
# @param str [String]
|
154
|
+
# @param separator [String] single-character string
|
155
|
+
# @return [Array<String>]
|
156
|
+
def escaped_split(str, separator)
|
157
|
+
unescaped_separator = /(?<!\\)(#{Regexp.escape(separator)})/
|
158
|
+
# String#split(<Regexp>) on non zero-width matches yields the match
|
159
|
+
# as the even entries in the array.
|
160
|
+
parts = str.split(unescaped_separator).each_slice(2).map(&:first)
|
161
|
+
parts.map do |part|
|
162
|
+
unescape(part, separator)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Utility method for joining a pre-escaped string with a not-yet escaped
|
167
|
+
# string on a given separator, escaping the new part before joining.
|
168
|
+
# @param pre_escaped_prefix [String]
|
169
|
+
# @param new_part [String] - will be escaped before joining
|
170
|
+
# @param separator [String] single-character string
|
171
|
+
# @return [String]
|
172
|
+
def escaped_join(pre_escaped_prefix, new_part, separator)
|
173
|
+
[pre_escaped_prefix, escape(new_part, separator)].compact.join(separator)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
extend UtilityMethods
|
178
|
+
end
|
data/sparsify.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sparsify/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'sparsify'
|
8
|
+
spec.version = Sparsify::VERSION
|
9
|
+
spec.authors = ['Ryan Biesemeyer']
|
10
|
+
spec.email = ['ryan@simplymeasured.com']
|
11
|
+
spec.description = 'Flattens deeply-nested hashes into sparse hashes'
|
12
|
+
spec.summary = 'Flattens deeply-nested hashes into sparse hashes'
|
13
|
+
spec.homepage = 'https://www.github.com/simplymeasured/sparsify'
|
14
|
+
spec.license = 'Apache 2'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'ruby-appraiser-rubocop'
|
25
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'sparsify'
|
3
|
+
|
4
|
+
describe 'Sparsify' do
|
5
|
+
let(:source_hash) do
|
6
|
+
{'foo' => { 'bar' => {'baz'=>'bingo', 'whee'=> {}}},'asdf'=>'qwer'}
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:intended_result) do
|
10
|
+
{
|
11
|
+
'foo.bar.baz' => 'bingo',
|
12
|
+
'foo.bar.whee' => {},
|
13
|
+
'asdf' => 'qwer',
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should sparsify' do
|
18
|
+
Sparsify(source_hash).should == intended_result
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'round-trip' do
|
22
|
+
subject do
|
23
|
+
Unsparsify(Sparsify(source_hash, separator: '|'), separator: '|')
|
24
|
+
end
|
25
|
+
it { should == source_hash }
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'escaping separator in keys' do
|
29
|
+
let(:nested_hash) do
|
30
|
+
{
|
31
|
+
'foo.foo' => 'foo',
|
32
|
+
'foo' => {'bar.bar' => 'bar'}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
let(:sparse_hash) do
|
36
|
+
{
|
37
|
+
'foo\.foo' => 'foo',
|
38
|
+
'foo.bar\.bar' => 'bar'
|
39
|
+
}
|
40
|
+
end
|
41
|
+
context '#sparse' do
|
42
|
+
subject do
|
43
|
+
Sparsify(nested_hash)
|
44
|
+
end
|
45
|
+
it { should eq sparse_hash }
|
46
|
+
end
|
47
|
+
context '#unsparse' do
|
48
|
+
subject do
|
49
|
+
Unsparsify(sparse_hash)
|
50
|
+
end
|
51
|
+
it { should eq nested_hash }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'sparse_array' do
|
56
|
+
let(:source_hash) do
|
57
|
+
{'foo' => ['bar','baz',{'bingo'=>'baby'}]}
|
58
|
+
end
|
59
|
+
let(:intended_result) do
|
60
|
+
{
|
61
|
+
'foo.0' => 'bar',
|
62
|
+
'foo.1' => 'baz',
|
63
|
+
'foo.2.bingo' => 'baby'
|
64
|
+
}
|
65
|
+
end
|
66
|
+
it 'should sparsify' do
|
67
|
+
Sparsify(source_hash, sparse_array: true).should == intended_result
|
68
|
+
end
|
69
|
+
context 'round-trip' do
|
70
|
+
subject do
|
71
|
+
Unsparsify(Sparsify(source_hash, sparse_array: true), sparse_array: true)
|
72
|
+
end
|
73
|
+
it { should == source_hash }
|
74
|
+
end
|
75
|
+
context 'zero-pad' do
|
76
|
+
let(:source_hash) do
|
77
|
+
{'foo' => ['bar','baz',{'bingo'=>'baby'},'blip','blip','blip','blip','blip','blip','blip','blip']}
|
78
|
+
end
|
79
|
+
let(:intended_result) do
|
80
|
+
{
|
81
|
+
'foo.00' => 'bar',
|
82
|
+
'foo.01' => 'baz',
|
83
|
+
'foo.02.bingo' => 'baby',
|
84
|
+
'foo.03' => 'blip',
|
85
|
+
'foo.04' => 'blip',
|
86
|
+
'foo.05' => 'blip',
|
87
|
+
'foo.06' => 'blip',
|
88
|
+
'foo.07' => 'blip',
|
89
|
+
'foo.08' => 'blip',
|
90
|
+
'foo.09' => 'blip',
|
91
|
+
'foo.10' => 'blip',
|
92
|
+
}
|
93
|
+
end
|
94
|
+
it 'should sparsify' do
|
95
|
+
Sparsify(source_hash, sparse_array: :zero_pad).should == intended_result
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context '.sparse_each' do
|
101
|
+
context 'when no block given' do
|
102
|
+
let(:sparse_each_result) { Sparsify.sparse_each(source_hash) }
|
103
|
+
context 'the result' do
|
104
|
+
subject { sparse_each_result }
|
105
|
+
it { should be_an_instance_of Enumerator }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with block given' do
|
110
|
+
it 'should yield all entries from the sparse hash' do
|
111
|
+
expect do |b|
|
112
|
+
Sparsify.sparse_each(source_hash, &b)
|
113
|
+
end.to yield_successive_args(*intended_result.to_a)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context '.sparse_fetch' do
|
119
|
+
let(:fetch_args) { [source_hash, search_key] }
|
120
|
+
let(:the_intended_result) { intended_result[search_key] }
|
121
|
+
let(:the_fetcher) do
|
122
|
+
proc { |block| Sparsify.sparse_fetch(*fetch_args, &block) }
|
123
|
+
end
|
124
|
+
let(:the_result) { the_fetcher.call }
|
125
|
+
let(:fetch_block) { nil }
|
126
|
+
context 'when fetching an existing key' do
|
127
|
+
let(:search_key) { 'foo.bar.baz' }
|
128
|
+
context 'the result' do
|
129
|
+
subject { the_result }
|
130
|
+
it { should == the_intended_result }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
context 'when fetching an existing partial key' do
|
134
|
+
context 'the result' do
|
135
|
+
let(:search_key) { 'foo.bar' }
|
136
|
+
let(:the_intended_result) { source_hash['foo']['bar'] }
|
137
|
+
subject { the_result }
|
138
|
+
it { should eq the_intended_result }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
context 'when fetching a missing key' do
|
142
|
+
let(:search_key) { 'fiddle.foodle' }
|
143
|
+
context 'with default supplied' do
|
144
|
+
let(:fetch_args) { [source_hash, search_key, default_value] }
|
145
|
+
let(:default_value) { :some_default }
|
146
|
+
it 'should return the default' do
|
147
|
+
the_result.should eq default_value
|
148
|
+
end
|
149
|
+
end
|
150
|
+
context 'with alternate block supplied' do
|
151
|
+
it 'should yield the block' do
|
152
|
+
expect { |b| the_fetcher.call(b) }.to yield_with_no_args
|
153
|
+
end
|
154
|
+
context 'the return value' do
|
155
|
+
let(:default_value) { :some_default }
|
156
|
+
let(:default_proc) { proc { default_value } }
|
157
|
+
subject { the_fetcher.call(default_proc)}
|
158
|
+
it { should eq default_value }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
specify { expect { the_result }.to raise_exception KeyError }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context '.sparse_get' do
|
166
|
+
let(:get_args) { [source_hash, search_key] }
|
167
|
+
let(:the_getter) do
|
168
|
+
proc { |block| Sparsify.sparse_fetch(*get_args, &block) }
|
169
|
+
end
|
170
|
+
let(:the_result) { the_getter.call }
|
171
|
+
|
172
|
+
context 'when getting an existing key' do
|
173
|
+
let(:the_intended_result) { intended_result[search_key] }
|
174
|
+
let(:search_key) { 'foo.bar.baz' }
|
175
|
+
context 'the result' do
|
176
|
+
subject { the_result }
|
177
|
+
it { should == the_intended_result }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'when getting an existing partial key' do
|
182
|
+
context 'the result' do
|
183
|
+
let(:search_key) { 'foo.bar' }
|
184
|
+
let(:the_intended_result) { source_hash['foo']['bar'] }
|
185
|
+
subject { the_result }
|
186
|
+
it { should eq the_intended_result }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'when getting a missing key' do
|
191
|
+
let(:search_key) { 'fiddle.foodle' }
|
192
|
+
context 'the result' do
|
193
|
+
subject { the_result }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sparsify
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Biesemeyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-12-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: ruby-appraiser-rubocop
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Flattens deeply-nested hashes into sparse hashes
|
79
|
+
email:
|
80
|
+
- ryan@simplymeasured.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- CONTRIBUTING.md
|
87
|
+
- Gemfile
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- git_hooks/pre-commit/ruby-appraiser
|
92
|
+
- lib/sparsify.rb
|
93
|
+
- lib/sparsify/guard_methods.rb
|
94
|
+
- lib/sparsify/helper_methods.rb
|
95
|
+
- lib/sparsify/utility_methods.rb
|
96
|
+
- lib/sparsify/version.rb
|
97
|
+
- sparsify.gemspec
|
98
|
+
- spec/sparsify_spec.rb
|
99
|
+
homepage: https://www.github.com/simplymeasured/sparsify
|
100
|
+
licenses:
|
101
|
+
- Apache 2
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 1.8.24
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Flattens deeply-nested hashes into sparse hashes
|
124
|
+
test_files:
|
125
|
+
- spec/sparsify_spec.rb
|
126
|
+
has_rdoc:
|