depth 0.0.1 → 0.0.2
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 +4 -4
- data/.rspec +2 -0
- data/README.md +239 -0
- data/lib/depth/actions.rb +11 -5
- data/lib/depth/complex_hash.rb +2 -1
- data/lib/depth/enumeration/enumerable.rb +70 -60
- data/lib/depth/enumeration/node.rb +1 -1
- data/lib/depth/route_element.rb +2 -2
- data/lib/depth/version.rb +1 -1
- data/lib/depth.rb +8 -1
- data/spec/depth/actions_spec.rb +118 -0
- data/spec/depth/complex_hash_spec.rb +30 -0
- data/spec/depth/enumerable_spec.rb +217 -0
- data/spec/depth/route_element_spec.rb +82 -0
- data/spec/spec_helper.rb +72 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e40cab4a65c17f79c8dd51b99c7a5abd34c254a
|
4
|
+
data.tar.gz: 7e39b1a02e7ccbe5e2c40a89b489b30b5bc3bc8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd73d5e07c490e333b57e2206b01619c341d9702a0dd9139d9d911a7bc306bf43062a60e0e85310adf025ecf5fc939cda514e1051dc069a5545af1a235baf395
|
7
|
+
data.tar.gz: f090c3154f0fb9c2846ee0e7ffb1333be42a2902cf65bcca4e9e2d6fda61d870ed08c4c9d94bd0d856d24ea66f58a6b1cfeba621262c4c5835a774fdf5ff2773
|
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,3 +1,242 @@
|
|
1
1
|
# Depth
|
2
2
|
|
3
|
+
Depth is a utility gem for deep manipulation of complex hashes, that
|
4
|
+
is nested hash and array structures. As you have probably guessed it
|
5
|
+
was originally created to deal with a JSON like document structure.
|
6
|
+
Importantly it uses a non-recursive approach to its enumeration.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'depth'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install depth
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### The complex hash
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
hash = { '$and' => [
|
30
|
+
{ '#weather' => { 'something' => [], 'thisguy' => 4 } },
|
31
|
+
{ '$or' => [
|
32
|
+
{ '#otherfeed' => {'thing' => [] } },
|
33
|
+
]}
|
34
|
+
]}
|
35
|
+
```
|
36
|
+
_Nicked from a query engine we're using on Driftrock_
|
37
|
+
|
38
|
+
The above is a sample complex hash, to use the gem to
|
39
|
+
start manipulating it is pretty simple:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
complex_hash = Depth::ComplexHash.new(hash)
|
43
|
+
```
|
44
|
+
|
45
|
+
Not exactly rocket science (not even data science). You
|
46
|
+
can retrieve the hash with either `base` or `to_h`.
|
47
|
+
|
48
|
+
### Manipulation
|
49
|
+
|
50
|
+
Manipulation of the hash is done using routes. A route
|
51
|
+
being a description of how to traverse the hash to
|
52
|
+
get to this point.
|
53
|
+
|
54
|
+
The messages signatures relating to manipulation are:
|
55
|
+
|
56
|
+
* `set(route, value)` = Set a value
|
57
|
+
* `find(route)` = Find a value
|
58
|
+
* `alter(route, key:)` = Alter a key (the last key in route)
|
59
|
+
* `alter(route, value:)` = Alter a value, identical to `set`
|
60
|
+
* `alter(route, key: value:)` = Alter a key and value, identical to a `set` and then `delete`
|
61
|
+
* `delete(route)` = Delete a value
|
62
|
+
|
63
|
+
Routes can be defined as an array of keys or indeces:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
hash = { '$and' => [
|
67
|
+
{ '#weather' => { 'something' => [], 'thisguy' => 4 } },
|
68
|
+
{ '$or' => [
|
69
|
+
{ '#otherfeed' => {'thing' => [] } },
|
70
|
+
]}
|
71
|
+
]}
|
72
|
+
route = ['$and', 1, '$or', 0, '#otherfeed', 'thing']
|
73
|
+
Depth::ComplexHash.new(hash).find(route) # => []
|
74
|
+
```
|
75
|
+
|
76
|
+
But there's something cool hidden in the `set` message,
|
77
|
+
if part of the structure is missing, it'll fill it in as it
|
78
|
+
goes, e.g.:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
hash = { '$and' => [
|
82
|
+
{ '#weather' => { 'something' => [], 'thisguy' => 4 } },
|
83
|
+
{ '$or' => [
|
84
|
+
{ '#otherfeed' => {'thing' => [] } },
|
85
|
+
]}
|
86
|
+
]}
|
87
|
+
route = ['$and', 1, '$or', 0, '#sup', 'thisthing']
|
88
|
+
Depth::ComplexHash.new(hash).set(route, 'hello')
|
89
|
+
puts hash.inspect #=>
|
90
|
+
# hash = { '$and' => [
|
91
|
+
# { '#weather' => { 'something' => [], 'thisguy' => 4 } },
|
92
|
+
# { '$or' => [
|
93
|
+
# { '#otherfeed' => {'thing' => [] } },
|
94
|
+
# { '#sup' => {'thisthing' => 'hello' } },
|
95
|
+
# ]}
|
96
|
+
# ]}
|
97
|
+
```
|
98
|
+
|
99
|
+
Great if you want it to be a hash, but what if you want to add
|
100
|
+
an array, no worries, just say so in the route:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
route = ['$and', 1, '$or', 0, ['#sup', :array], 0]
|
104
|
+
# Routes can also be defined in other ways
|
105
|
+
route = ['$and', 1, '$or', 0, { key: '#sup', type: :array }, 0]
|
106
|
+
route = ['$and', 1, '$or', 0, RouteElement.new('#sup', type: :array), 0]
|
107
|
+
```
|
108
|
+
|
109
|
+
### Enumeration
|
110
|
+
|
111
|
+
The messages signatures relating to enumeration are:
|
112
|
+
|
113
|
+
* `each` = yields `key_or_index` and `fragment`, returns the complex hash
|
114
|
+
* `map` = yields `key_or_index`, `fragment` and `parent_type`, returns a new complex hash
|
115
|
+
* `map_values` = yields `fragment`, returns a new complex hash
|
116
|
+
* `map_keys` = yields `key_or_index`, returns a new complex hash
|
117
|
+
* `map!`, `map_keys!` and `map_keys_and_values!`, returns a new complex hash
|
118
|
+
* `reduce(memo)` = yields `memo`, `key` and `fragment`, returns memo
|
119
|
+
* `each_with_object(obj)` = yields `key`, `fragment` and `object`, returns object
|
120
|
+
|
121
|
+
_Fragment refers to a chunk of the original hash_
|
122
|
+
|
123
|
+
These, perhaps, require a bit more explanation:
|
124
|
+
|
125
|
+
#### each
|
126
|
+
|
127
|
+
The staple, and arguably the most important, of all the enumeration methods,
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
hash = { ... }
|
131
|
+
Depth::ComplexHash.new(hash).each { |key, fragment| }
|
132
|
+
```
|
133
|
+
|
134
|
+
Each yields keys and associated fragments from the leaf nodes
|
135
|
+
backwards. For example, the hash:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
{ '$and' => [{ 'something' => { 'x' => 4 } }] }
|
139
|
+
```
|
140
|
+
|
141
|
+
would yield:
|
142
|
+
|
143
|
+
1. `x, 4`
|
144
|
+
2. `something, { "x" => 4 }`
|
145
|
+
3. `0, { "something" => { "x" => 4 } }`
|
146
|
+
4. `$and, [{ "something" => { "x" => 4 } }]`
|
147
|
+
|
148
|
+
|
149
|
+
#### map
|
150
|
+
|
151
|
+
Map yields both the current key/index and the current fragment,
|
152
|
+
expecting both returned in an array. I've yet to decide if
|
153
|
+
there should be a third argument that tells you whether or not
|
154
|
+
the key/index is for an array or a hash. I've not needed it
|
155
|
+
but I suspect it might be useful. If it comes up I'll add it.
|
156
|
+
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
|
160
|
+
Depth::ComplexHash.new(hash).map do |key, fragment|
|
161
|
+
[key, fragment]
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
like `each` the above would yield:
|
166
|
+
|
167
|
+
1. `x, 4`
|
168
|
+
2. `something, { "x" => 4 }`
|
169
|
+
3. `0, { "something" => { "x" => 4 } }`
|
170
|
+
4. `$and, [{ "something" => { "x" => 4 } }]`
|
171
|
+
|
172
|
+
and with the contents being unchanged it would return a
|
173
|
+
new complex hash with equal contents to the current one.
|
174
|
+
|
175
|
+
#### map_values
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
|
179
|
+
Depth::ComplexHash.new(hash).map_values do |fragment|
|
180
|
+
fragment
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
This will yield only the fragments from `map`, useful if
|
185
|
+
you only wish to alter the value parts of the hash.
|
186
|
+
|
187
|
+
#### map_keys
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
|
191
|
+
Depth::ComplexHash.new(hash).map_keys do |key|
|
192
|
+
key
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
This will yield only the keys from `map`, useful if
|
197
|
+
you only wish to alter the keys.
|
198
|
+
|
199
|
+
#### map!, map_keys!, map_values!
|
200
|
+
|
201
|
+
The same as their non-exclamation marked siblings save that
|
202
|
+
they will cause the complex hash on which they operate to change.
|
203
|
+
|
204
|
+
#### reduce and each_with_object
|
205
|
+
|
206
|
+
Operate as you would expect. Can I take a moment to point out how
|
207
|
+
irritating it is that `each_with_object` yields the object you pass
|
208
|
+
in as its last argument while `reduce` yields it as its first O_o?
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
hash = { '$and' => [{ 'something' => { 'x' => 4 } }] }
|
212
|
+
Depth::ComplexHash.new(hash).reduce(0) do |memo, key, fragment|
|
213
|
+
memo += 1
|
214
|
+
end
|
215
|
+
|
216
|
+
Depth::ComplexHash.new(hash).each_with_object([]) do |key, fragment, obj|
|
217
|
+
obj << key
|
218
|
+
end
|
219
|
+
```
|
220
|
+
|
221
|
+
## Why?
|
222
|
+
|
223
|
+
Alright, we needed to be able to find certain keys
|
224
|
+
from all the keys contained within the complex hash as said keys
|
225
|
+
were the instructions as to what data the hash would be able to match
|
226
|
+
against. This peice of code was originally recursive. We were adding
|
227
|
+
a feature that required us to also be able to edit these keys, mark
|
228
|
+
them with a unique identifier. As I was writing this I decided I wasn't
|
229
|
+
happy with the recursive nature of the key search as we have no guarantees
|
230
|
+
about how nested the hash could be. As I refactored the find and built
|
231
|
+
the edit it became obvious that the code wasn't tied to the project at
|
232
|
+
hand so I refactored it out to here.
|
233
|
+
|
234
|
+
|
235
|
+
## Contributing
|
236
|
+
|
237
|
+
1. Fork it ( https://github.com/[my-github-username]/depth/fork )
|
238
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
239
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
240
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
241
|
+
5. Create a new Pull Request
|
3
242
|
|
data/lib/depth/actions.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
module Depth
|
2
2
|
module Actions
|
3
|
+
#:nocov:
|
4
|
+
def base
|
5
|
+
raise NoMethodError.new('should be overridden')
|
6
|
+
end
|
7
|
+
#:nocov:
|
8
|
+
|
3
9
|
def set(route, value)
|
4
10
|
route = RouteElement.convert_route(route)
|
5
|
-
object = route[0 ... -1].reduce(Traverser.new(
|
11
|
+
object = route[0 ... -1].reduce(Traverser.new(base)) { |t, route_el|
|
6
12
|
t.next_or_create(route_el.key) { route_el.create }
|
7
13
|
}.object
|
8
14
|
object[route.last.key] = value
|
@@ -10,7 +16,7 @@ module Depth
|
|
10
16
|
|
11
17
|
def find(route)
|
12
18
|
route = RouteElement.convert_route(route)
|
13
|
-
route.reduce(Traverser.new(
|
19
|
+
route.reduce(Traverser.new(base)) { |t, route_el|
|
14
20
|
t.next(route_el.key)
|
15
21
|
}.object
|
16
22
|
end
|
@@ -19,16 +25,16 @@ module Depth
|
|
19
25
|
return set(route, value) if key == nil
|
20
26
|
route = RouteElement.convert_route(route)
|
21
27
|
value = find(route) unless value
|
22
|
-
new_route = (route[0 ... -1] << RouteElement.convert(
|
28
|
+
new_route = (route[0 ... -1] << RouteElement.convert(key))
|
23
29
|
set(new_route, value) # ensure it exists
|
24
30
|
old_key = route.last.key
|
25
|
-
return unless old_key !=
|
31
|
+
return unless old_key != key
|
26
32
|
delete(route)
|
27
33
|
end
|
28
34
|
|
29
35
|
def delete(route)
|
30
36
|
route = RouteElement.convert_route(route)
|
31
|
-
traverser = route[0...-1].reduce(Traverser.new(
|
37
|
+
traverser = route[0...-1].reduce(Traverser.new(base)) do |t, route_el|
|
32
38
|
t.next(route_el.key)
|
33
39
|
end
|
34
40
|
if traverser.array?
|
data/lib/depth/complex_hash.rb
CHANGED
@@ -1,82 +1,92 @@
|
|
1
1
|
module Depth
|
2
|
-
module
|
2
|
+
module Enumeration
|
3
|
+
module Enumerable
|
4
|
+
#:nocov:
|
5
|
+
def base
|
6
|
+
raise NoMethodError.new('should be overridden')
|
7
|
+
end
|
8
|
+
#:nocov:
|
3
9
|
|
4
|
-
|
5
|
-
|
6
|
-
|
10
|
+
def each_with_object(object, &block)
|
11
|
+
object.tap do |o|
|
12
|
+
each do |key, fragment|
|
13
|
+
block.call(key, fragment, o)
|
14
|
+
end
|
15
|
+
end
|
7
16
|
end
|
8
|
-
object
|
9
|
-
end
|
10
17
|
|
11
|
-
|
12
|
-
|
13
|
-
|
18
|
+
def reduce(memo, &block)
|
19
|
+
each do |key, fragment|
|
20
|
+
memo = block.call(memo, key, fragment)
|
21
|
+
end
|
22
|
+
memo
|
14
23
|
end
|
15
|
-
memo
|
16
|
-
end
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
def map_keys!(&block)
|
26
|
+
@base = map_keys(&block).base
|
27
|
+
self
|
28
|
+
end
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
30
|
+
def map_values!(&block)
|
31
|
+
@base = map_values(&block).base
|
32
|
+
self
|
33
|
+
end
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
|
35
|
+
def map!(&block)
|
36
|
+
@base = map(&block).base
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def map_keys(&block)
|
41
|
+
map do |key, fragment|
|
42
|
+
[block.call(key), fragment]
|
43
|
+
end
|
44
|
+
end
|
29
45
|
|
30
|
-
|
31
|
-
|
32
|
-
|
46
|
+
def map_values(&block)
|
47
|
+
map do |key, fragment, parent_type|
|
48
|
+
[key, block.call(fragment)]
|
49
|
+
end
|
33
50
|
end
|
34
|
-
end
|
35
51
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
52
|
+
def map(&block)
|
53
|
+
node_map do |node, new_q|
|
54
|
+
orig_key = node.parent_key
|
55
|
+
existing = new_q.find(node.route)
|
56
|
+
orig_fragment = existing.nil? ? node.fragment : existing
|
57
|
+
block.call(orig_key, orig_fragment)
|
58
|
+
end
|
40
59
|
end
|
41
|
-
end
|
42
60
|
|
43
|
-
|
44
|
-
|
45
|
-
orig_key = node.parent_key
|
46
|
-
existing = new_q.find(node.route)
|
47
|
-
orig_fragment = existing.nil? ? node.fragment : existing
|
48
|
-
next [orig_key, orig_fragment] unless node.parent.hash?
|
49
|
-
block.call(orig_key, orig_fragment)
|
61
|
+
def each(&block)
|
62
|
+
enumerate { |node| block.call(node.parent_key, node.fragment) }
|
50
63
|
end
|
51
|
-
end
|
52
64
|
|
53
|
-
|
54
|
-
enumerate { |node| block.call(node.parent_key, node.fragment) }
|
55
|
-
end
|
65
|
+
private
|
56
66
|
|
57
|
-
|
67
|
+
def node_map(&block)
|
68
|
+
new_q = self.class.new(base.class.new)
|
69
|
+
enumerate do |node|
|
70
|
+
key, val = block.call(node, new_q)
|
71
|
+
new_q.alter(node.route, key: key, value: val)
|
72
|
+
end
|
73
|
+
new_q
|
74
|
+
end
|
58
75
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
76
|
+
def enumerate
|
77
|
+
root = Node.new(nil, nil, base)
|
78
|
+
current = root
|
79
|
+
begin
|
80
|
+
if current.next?
|
81
|
+
current = current.next
|
82
|
+
elsif !current.root?
|
83
|
+
yield(current)
|
84
|
+
current = current.parent
|
85
|
+
end
|
86
|
+
end while !current.root? || current.next?
|
87
|
+
self
|
64
88
|
end
|
65
|
-
new_q
|
66
|
-
end
|
67
89
|
|
68
|
-
def enumerate
|
69
|
-
root = Node.new(nil, nil, query)
|
70
|
-
current = root
|
71
|
-
begin
|
72
|
-
if current.next?
|
73
|
-
current = current.next
|
74
|
-
elsif !current.root?
|
75
|
-
yield(current)
|
76
|
-
current = current.parent
|
77
|
-
end
|
78
|
-
end while !current.root? || current.next?
|
79
|
-
root.fragment
|
80
90
|
end
|
81
91
|
end
|
82
92
|
end
|
data/lib/depth/route_element.rb
CHANGED
@@ -22,9 +22,9 @@ module Depth
|
|
22
22
|
case el
|
23
23
|
when Array
|
24
24
|
type = el.count > 1 ? el[1] : :hash
|
25
|
-
RouteElement.new(
|
25
|
+
RouteElement.new(el[0], type: type)
|
26
26
|
when Hash
|
27
|
-
key_or_index = el.fetch(:key
|
27
|
+
key_or_index = el.fetch(:key) { el.fetch(:index) }
|
28
28
|
RouteElement.new(key_or_index, type: el.fetch(:type, :hash))
|
29
29
|
else
|
30
30
|
RouteElement.new(el)
|
data/lib/depth/version.rb
CHANGED
data/lib/depth.rb
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
|
1
|
+
require_relative './depth/version'
|
2
2
|
|
3
3
|
module Depth
|
4
4
|
end
|
5
|
+
|
6
|
+
require_relative './depth/enumeration/enumerable'
|
7
|
+
require_relative './depth/enumeration/node'
|
8
|
+
require_relative './depth/actions'
|
9
|
+
require_relative './depth/traverser'
|
10
|
+
require_relative './depth/route_element'
|
11
|
+
require_relative './depth/complex_hash'
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Depth
|
4
|
+
RSpec.describe Actions do
|
5
|
+
|
6
|
+
let(:actions_class) do
|
7
|
+
Class.new do
|
8
|
+
include Actions
|
9
|
+
attr_reader :base
|
10
|
+
def initialize(base)
|
11
|
+
@base = base
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:hash) do
|
17
|
+
{ '$and' => [
|
18
|
+
{ '#weather' => { 'something' => [] } },
|
19
|
+
{ '#weather' => { 'something' => [] } },
|
20
|
+
{ '#weather' => { 'something' => [] } },
|
21
|
+
{ '$or' => [
|
22
|
+
{ '#otherfeed' => {'thing' => [] } },
|
23
|
+
]}
|
24
|
+
]}
|
25
|
+
end
|
26
|
+
|
27
|
+
subject { actions_class.new(hash) }
|
28
|
+
|
29
|
+
describe '#set' do
|
30
|
+
it 'should let me set an existing value' do
|
31
|
+
route = [['$and', :array], [0, :hash],
|
32
|
+
['#weather', :hash], ['something', :array]]
|
33
|
+
expect do
|
34
|
+
subject.set(route, :test)
|
35
|
+
end.to change { hash['$and'][0]['#weather']['something'] }.to(:test)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should let me set a new value' do
|
39
|
+
route = [['$rargh', :array], [0, :hash],
|
40
|
+
['#weather', :hash], ['something', :array]]
|
41
|
+
subject.set(route, :test)
|
42
|
+
expect(hash['$rargh'][0]['#weather']['something']).to eq :test
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#delete' do
|
47
|
+
context 'when in a hash' do
|
48
|
+
let(:route) do
|
49
|
+
['$and', 1, '#weather']
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should let me delete a route endpoint' do
|
53
|
+
expect do
|
54
|
+
subject.delete(route)
|
55
|
+
end.to change { hash['$and'][1].empty? }.to(true)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'when in an array' do
|
60
|
+
let(:route) do
|
61
|
+
['$and', 1]
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should let me delete a route endpoint' do
|
65
|
+
expect do
|
66
|
+
subject.delete(route)
|
67
|
+
end.to change { hash['$and'].count }.by(-1)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#alter' do
|
73
|
+
let(:route) do
|
74
|
+
[['$and', :array], [0, :hash], ['#weather', :hash], ['something', :array]]
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should let me change a key' do
|
78
|
+
expect do
|
79
|
+
subject.alter(route, key: 'blah')
|
80
|
+
end.to change { hash['$and'][0]['#weather'].keys }.to ['blah']
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should let me change a value' do
|
84
|
+
expect do
|
85
|
+
subject.alter(route, value: 'rargh')
|
86
|
+
end.to change { hash['$and'][0]['#weather']['something'] }.to 'rargh'
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when changing key and value' do
|
90
|
+
it 'should set the new value' do
|
91
|
+
expect do
|
92
|
+
subject.alter(route, key: 'blah', value: 'rargh')
|
93
|
+
end.to change { hash['$and'][0]['#weather']['blah'] }.to 'rargh'
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'should delete the old key' do
|
97
|
+
expect do
|
98
|
+
subject.alter(route, key: 'blah', value: 'rargh')
|
99
|
+
end.to change { hash['$and'][0]['#weather'].key?('something') }.to false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#find' do
|
105
|
+
it 'should let me find an existing value' do
|
106
|
+
route = [['$and', :array], [0, :hash],
|
107
|
+
['#weather', :hash], ['something', :array]]
|
108
|
+
expect(subject.find(route)).to eq []
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should return nil if element does not exist' do
|
112
|
+
route = [['$rargh', :array], [0, :hash],
|
113
|
+
['#weather', :hash], ['something', :array]]
|
114
|
+
expect(subject.find(route)).to be_nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Depth
|
4
|
+
RSpec.describe ComplexHash do
|
5
|
+
let(:hash) do
|
6
|
+
{ '$and' => [
|
7
|
+
{ '#weather' => { 'something' => [] } },
|
8
|
+
{ '#weather' => { 'something' => [] } },
|
9
|
+
{ '#weather' => { 'something' => [] } },
|
10
|
+
{ '$or' => [
|
11
|
+
{ '#otherfeed' => {'thing' => [] } },
|
12
|
+
]}
|
13
|
+
]}
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { described_class.new(hash) }
|
17
|
+
|
18
|
+
describe '#base' do
|
19
|
+
it 'should return the underlying hash' do
|
20
|
+
expect(subject.base).to eq hash
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#to_h' do
|
25
|
+
it 'should be aliased to base' do
|
26
|
+
expect(subject.to_h).to be subject.base
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'pry-byebug'
|
4
|
+
|
5
|
+
module Depth::Enumeration
|
6
|
+
RSpec.describe Enumerable do
|
7
|
+
|
8
|
+
let(:enumerable_class) do
|
9
|
+
Class.new do
|
10
|
+
include Depth::Actions
|
11
|
+
include Depth::Enumeration::Enumerable
|
12
|
+
attr_reader :base
|
13
|
+
def initialize(base)
|
14
|
+
@base = base
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:hash) do
|
20
|
+
{ '$and' => [
|
21
|
+
{ '#weather' => { 'something' => [] } },
|
22
|
+
{ '$or' => [
|
23
|
+
{ '#otherfeed' => {'thing' => [] } },
|
24
|
+
]}
|
25
|
+
]}
|
26
|
+
end
|
27
|
+
|
28
|
+
subject { enumerable_class.new(hash) }
|
29
|
+
|
30
|
+
describe '#each_with_object' do
|
31
|
+
it "performs as you'd expect reduce to" do
|
32
|
+
keys = subject.each_with_object([]) do |key, fragment, obj|
|
33
|
+
obj << key if key.is_a?(String)
|
34
|
+
end
|
35
|
+
expected = ['something', '#weather', 'thing', '#otherfeed', '$or', '$and']
|
36
|
+
expect(keys).to eq expected
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#reduce' do
|
41
|
+
it "performs as you'd expect reduce to" do
|
42
|
+
keys = subject.reduce(0) do |sum, key, fragment|
|
43
|
+
sum += (key.is_a?(String) ? 1 : 0)
|
44
|
+
end
|
45
|
+
expect(keys).to eq 6
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
shared_examples 'it maps changing self' do
|
50
|
+
let(:map_block) { proc { |x| x } }
|
51
|
+
let(:alter_map_block) { proc { |x| 'rarg' } }
|
52
|
+
|
53
|
+
it 'should return self' do
|
54
|
+
result = subject.send(map_message, &map_block)
|
55
|
+
expect(result).to be subject
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'with alteration' do
|
59
|
+
it 'should change base' do
|
60
|
+
expect do
|
61
|
+
subject.send(map_message, &alter_map_block)
|
62
|
+
end.to change { subject.base }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'without alteration' do
|
67
|
+
it 'should not change contents' do
|
68
|
+
expect do
|
69
|
+
subject.send(map_message, &map_block)
|
70
|
+
end.to_not change { subject.base }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#map!' do
|
76
|
+
it_behaves_like 'it maps changing self' do
|
77
|
+
let(:map_message) { 'map!' }
|
78
|
+
let(:map_block) { proc { |x, y| [x, y] } }
|
79
|
+
let(:alter_map_block) do
|
80
|
+
proc { |k, v|
|
81
|
+
next [k, v] unless k.is_a?(String)
|
82
|
+
["#{k}rargh", v]
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#map_keys!' do
|
89
|
+
it_behaves_like 'it maps changing self' do
|
90
|
+
let(:map_message) { 'map_keys!' }
|
91
|
+
let(:alter_map_block) do
|
92
|
+
proc { |k|
|
93
|
+
next k unless k.is_a?(String)
|
94
|
+
"#{k}Altered"
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#map_values!' do
|
101
|
+
it_behaves_like 'it maps changing self' do
|
102
|
+
let(:map_message) { 'map_values!' }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
shared_examples 'it maps to a new object' do
|
107
|
+
let(:map_block) { proc { |x| x } }
|
108
|
+
it 'should return a new object' do
|
109
|
+
result = subject.send(map_message, &map_block)
|
110
|
+
expect(result).to_not be subject
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should return an object of the same class' do
|
114
|
+
result = subject.send(map_message, &map_block)
|
115
|
+
expect(result.class).to be subject.class
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'without alteration' do
|
119
|
+
it 'should return an object with the same contents' do
|
120
|
+
result = subject.send(map_message, &map_block)
|
121
|
+
expect(result.base).to eq subject.base
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '#map' do
|
127
|
+
it_behaves_like 'it maps to a new object' do
|
128
|
+
let(:map_message) { :map }
|
129
|
+
let(:map_block) { proc { |x, y| [x, y] } }
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'with alteration' do
|
133
|
+
let(:result) do
|
134
|
+
subject.map do |k, v|
|
135
|
+
next [k, v] unless k.is_a?(String)
|
136
|
+
["#{k}Altered", 'redacted']
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'should differ in the expected fashion' do
|
141
|
+
expected = { "$andAltered" => 'redacted' }
|
142
|
+
expect(result.base).to eq expected
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#map_keys' do
|
148
|
+
it_behaves_like 'it maps to a new object' do
|
149
|
+
let(:map_message) { :map_keys }
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'with alteration' do
|
153
|
+
let(:result) do
|
154
|
+
subject.map_keys do |k|
|
155
|
+
next k unless k.is_a?(String)
|
156
|
+
"#{k}Altered"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should differ in the expected fashion' do
|
161
|
+
expected = { "$andAltered" => [
|
162
|
+
{ "#weatherAltered" => { "somethingAltered" => [] } },
|
163
|
+
{ "$orAltered" => [ { "#otherfeedAltered" => { "thingAltered" => [] } } ] } ]
|
164
|
+
}
|
165
|
+
expect(result.base).to eq expected
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe '#map_values' do
|
171
|
+
it_behaves_like 'it maps to a new object' do
|
172
|
+
let(:map_message) { :map_values }
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'with alteration' do
|
176
|
+
let(:result) do
|
177
|
+
subject.map_values do |f|
|
178
|
+
'altered' if f.is_a?(Hash)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should differ in the expected fashion' do
|
183
|
+
expected = { "$and" => [ 'altered', 'altered' ] }
|
184
|
+
expect(result.base).to eq expected
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'should return an object with the different contents' do
|
188
|
+
expect(result.base).to_not eq subject.base
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
describe '#each' do
|
195
|
+
it 'should sort through all keys and values' do
|
196
|
+
enumerated = []
|
197
|
+
subject.each do |key, fragment|
|
198
|
+
enumerated << [key, fragment]
|
199
|
+
end
|
200
|
+
expected = [
|
201
|
+
['something', []],
|
202
|
+
['#weather', { 'something' => [] }],
|
203
|
+
[0, { '#weather' => { 'something' => [] }}],
|
204
|
+
['thing', []],
|
205
|
+
['#otherfeed', { 'thing' => [] }],
|
206
|
+
[0, { '#otherfeed' => { 'thing' => [] } }],
|
207
|
+
['$or', [{ '#otherfeed' => { 'thing' => [] } }]],
|
208
|
+
[1, { '$or' => [{ '#otherfeed' => { 'thing' => [] } }] }],
|
209
|
+
['$and', [{ '#weather' => { 'something' => [] }},
|
210
|
+
{ '$or' => [{ '#otherfeed' => { 'thing' => [] } }] }]]
|
211
|
+
]
|
212
|
+
expect(enumerated).to eq expected
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Depth
|
4
|
+
RSpec.describe RouteElement do
|
5
|
+
describe '::convert' do
|
6
|
+
let(:el) { nil }
|
7
|
+
let(:result) { described_class.convert(el) }
|
8
|
+
context 'with any random thing' do
|
9
|
+
let(:el) { 4 }
|
10
|
+
it 'should set the type hash' do
|
11
|
+
expect(result.type).to eq :hash
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should set the key as the element' do
|
15
|
+
expect(result.key).to eq el
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with a hash' do
|
20
|
+
let(:el) { { key: 'x', type: :array } }
|
21
|
+
|
22
|
+
it 'should set the type as the passed type' do
|
23
|
+
expect(result.type).to eq el.fetch(:type)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should set the key as the passed key' do
|
27
|
+
expect(result.key).to eq el.fetch(:key)
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with no type' do
|
31
|
+
let(:el) { { key: 'x' } }
|
32
|
+
|
33
|
+
it 'should set the type hash' do
|
34
|
+
expect(result.type).to eq :hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'with index instead of key' do
|
39
|
+
let(:el) { { index: 'x', type: :array } }
|
40
|
+
|
41
|
+
it 'should set the key as the passed index' do
|
42
|
+
expect(result.key).to eq el.fetch(:index)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'with an array with' do
|
48
|
+
context 'with two elements' do
|
49
|
+
let(:el) { ['x', :array] }
|
50
|
+
|
51
|
+
it 'should set the type as the second element' do
|
52
|
+
expect(result.type).to eq el[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should set the key as the first element' do
|
56
|
+
expect(result.key).to eq el[0]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with one element' do
|
61
|
+
let(:el) { ['x'] }
|
62
|
+
|
63
|
+
it 'should set the type hash' do
|
64
|
+
expect(result.type).to eq :hash
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should set the key as the element' do
|
68
|
+
expect(result.key).to eq el[0]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'with a route element' do
|
74
|
+
let(:el) { RouteElement.new('x') }
|
75
|
+
|
76
|
+
it 'should return the element' do
|
77
|
+
expect(result).to be el
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
SimpleCov.start
|
3
|
+
require './lib/depth'
|
4
|
+
RSpec.configure do |config|
|
5
|
+
# rspec-expectations config goes here. You can use an alternate
|
6
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
7
|
+
# assertions if you prefer.
|
8
|
+
config.expect_with :rspec do |expectations|
|
9
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
10
|
+
# and `failure_message` of custom matchers include text for helper methods
|
11
|
+
# defined using `chain`, e.g.:
|
12
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
13
|
+
# # => "be bigger than 2 and smaller than 4"
|
14
|
+
# ...rather than:
|
15
|
+
# # => "be bigger than 2"
|
16
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
17
|
+
end
|
18
|
+
|
19
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
20
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
21
|
+
config.mock_with :rspec do |mocks|
|
22
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
23
|
+
# a real object. This is generally recommended, and will default to
|
24
|
+
# `true` in RSpec 4.
|
25
|
+
mocks.verify_partial_doubles = true
|
26
|
+
end
|
27
|
+
|
28
|
+
# The settings below are suggested to provide a good initial experience
|
29
|
+
# with RSpec, but feel free to customize to your heart's content.
|
30
|
+
=begin
|
31
|
+
# These two settings work together to allow you to limit a spec run
|
32
|
+
# to individual examples or groups you care about by tagging them with
|
33
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
34
|
+
# get run.
|
35
|
+
config.filter_run :focus
|
36
|
+
config.run_all_when_everything_filtered = true
|
37
|
+
|
38
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
39
|
+
# For more details, see:
|
40
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
41
|
+
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
42
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
43
|
+
config.disable_monkey_patching!
|
44
|
+
|
45
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
46
|
+
# file, and it's useful to allow more verbose output when running an
|
47
|
+
# individual spec file.
|
48
|
+
if config.files_to_run.one?
|
49
|
+
# Use the documentation formatter for detailed output,
|
50
|
+
# unless a formatter has already been configured
|
51
|
+
# (e.g. via a command-line flag).
|
52
|
+
config.default_formatter = 'doc'
|
53
|
+
end
|
54
|
+
|
55
|
+
# Print the 10 slowest examples and example groups at the
|
56
|
+
# end of the spec run, to help surface which specs are running
|
57
|
+
# particularly slow.
|
58
|
+
config.profile_examples = 10
|
59
|
+
|
60
|
+
# Run specs in random order to surface order dependencies. If you find an
|
61
|
+
# order dependency and want to debug it, you can fix the order by providing
|
62
|
+
# the seed, which is printed after each run.
|
63
|
+
# --seed 1234
|
64
|
+
config.order = :random
|
65
|
+
|
66
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
67
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
68
|
+
# test failures related to randomization by passing the same `--seed` value
|
69
|
+
# as the one that triggered the failure.
|
70
|
+
Kernel.srand config.seed
|
71
|
+
=end
|
72
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: depth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -88,6 +88,7 @@ extensions: []
|
|
88
88
|
extra_rdoc_files: []
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
|
+
- ".rspec"
|
91
92
|
- Gemfile
|
92
93
|
- LICENSE.txt
|
93
94
|
- README.md
|
@@ -101,6 +102,11 @@ files:
|
|
101
102
|
- lib/depth/route_element.rb
|
102
103
|
- lib/depth/traverser.rb
|
103
104
|
- lib/depth/version.rb
|
105
|
+
- spec/depth/actions_spec.rb
|
106
|
+
- spec/depth/complex_hash_spec.rb
|
107
|
+
- spec/depth/enumerable_spec.rb
|
108
|
+
- spec/depth/route_element_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
104
110
|
homepage: https://github.com/maxdupenois/depth
|
105
111
|
licenses:
|
106
112
|
- MIT
|
@@ -125,4 +131,9 @@ rubygems_version: 2.2.2
|
|
125
131
|
signing_key:
|
126
132
|
specification_version: 4
|
127
133
|
summary: Depth is a utility gem for dealing with nested hashes and arrays
|
128
|
-
test_files:
|
134
|
+
test_files:
|
135
|
+
- spec/depth/actions_spec.rb
|
136
|
+
- spec/depth/complex_hash_spec.rb
|
137
|
+
- spec/depth/enumerable_spec.rb
|
138
|
+
- spec/depth/route_element_spec.rb
|
139
|
+
- spec/spec_helper.rb
|