sparsify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: