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