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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
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
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+ source 'http://gems.production.int.simplymeasured.com:9292'
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in sparsify.gemspec
6
+ gemspec
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,9 @@
1
+ # encoding: utf-8
2
+ require 'bundler/gem_tasks'
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ spec.verbose = true
9
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module Sparsify
3
+ VERSION = '1.0.0'
4
+ 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: