abstractivator 0.0.22 → 0.0.23
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/lib/abstractivator/trees.rb +3 -220
- data/lib/abstractivator/trees/block_collector.rb +5 -1
- data/lib/abstractivator/trees/recursive_delete.rb +21 -0
- data/lib/abstractivator/trees/tree_compare.rb +125 -0
- data/lib/abstractivator/trees/tree_map.rb +121 -0
- data/lib/abstractivator/version.rb +1 -1
- data/spec/lib/abstractivator/trees/recursive_delete_spec.rb +28 -0
- data/spec/lib/abstractivator/{tree_visitor_spec.rb → trees/tree_compare_spec.rb} +1 -156
- data/spec/lib/abstractivator/trees/tree_map_spec.rb +201 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cf5eda70ec67c313b2df05ad3280682c8785f40
|
4
|
+
data.tar.gz: 9a0d1adaf5762a97cbf854086bb09edff5577169
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b04ee0b54f862440283a251cb940d05c55412d6dd9312286935302c9d26b9aeeeb892e3632cfdb433f3d7b9c65c7c804ca9a78e3080039d3cd19b709d928b4e3
|
7
|
+
data.tar.gz: 6d3f90b0717a52ef4ef069dea162070b3b0cf4bd7e96d36911475afeec513ef79a3b933152bcc3f488a300cf7caa4b1f46ec3b8858755b7e814bf2a18d20f7f5
|
data/lib/abstractivator/trees.rb
CHANGED
@@ -1,220 +1,3 @@
|
|
1
|
-
require '
|
2
|
-
require 'abstractivator/trees/
|
3
|
-
require '
|
4
|
-
require 'delegate'
|
5
|
-
require 'set'
|
6
|
-
|
7
|
-
module Abstractivator
|
8
|
-
module Trees
|
9
|
-
|
10
|
-
SetMask = Struct.new(:items, :get_key)
|
11
|
-
def set_mask(items, get_key)
|
12
|
-
SetMask.new(items, get_key)
|
13
|
-
end
|
14
|
-
|
15
|
-
def tree_compare(tree, mask, path=[], index=nil)
|
16
|
-
if mask == [:*] && tree.is_a?(Enumerable)
|
17
|
-
[]
|
18
|
-
elsif mask == :+ && tree != :__missing__
|
19
|
-
[]
|
20
|
-
elsif mask == :- && tree != :__missing__
|
21
|
-
[diff(path, tree, :__absent__)]
|
22
|
-
elsif mask.respond_to?(:call)
|
23
|
-
comparable = mask.call(tree)
|
24
|
-
comparable ? [] : [diff(path, tree, mask)]
|
25
|
-
else
|
26
|
-
case mask
|
27
|
-
when Hash
|
28
|
-
if tree.is_a?(Hash)
|
29
|
-
mask.each_pair.flat_map do |k, v|
|
30
|
-
tree_compare(tree.fetch(k, :__missing__), v, push_path(path, k))
|
31
|
-
end
|
32
|
-
else
|
33
|
-
[diff(path, tree, mask)]
|
34
|
-
end
|
35
|
-
when SetMask # must check this before Enumerable because Structs are enumerable
|
36
|
-
if tree.is_a?(Enumerable)
|
37
|
-
# convert the enumerables to hashes, then compare those hashes
|
38
|
-
tree_items = tree
|
39
|
-
mask_items = mask.items.dup
|
40
|
-
get_key = mask.get_key
|
41
|
-
|
42
|
-
be_strict = !mask_items.delete(:*)
|
43
|
-
new_tree = hashify_set(tree_items, get_key)
|
44
|
-
new_mask = hashify_set(mask_items, get_key)
|
45
|
-
tree_keys = Set.new(new_tree.keys)
|
46
|
-
mask_keys = Set.new(new_mask.keys)
|
47
|
-
tree_only = tree_keys - mask_keys
|
48
|
-
|
49
|
-
# report duplicate keys
|
50
|
-
if new_tree.size < tree_items.size
|
51
|
-
diff(path, [:__duplicate_keys__, duplicates(tree_items.map(&get_key))], nil)
|
52
|
-
elsif new_mask.size < mask_items.size
|
53
|
-
diff(path, nil, [:__duplicate_keys__, duplicates(mask_items.map(&get_key))])
|
54
|
-
# hash comparison allows extra values in the tree.
|
55
|
-
# report extra values in the tree unless there was a :* in the mask
|
56
|
-
elsif be_strict && tree_only.any?
|
57
|
-
tree_only.map{|k| diff(push_path(path, k), new_tree[k], :__absent__)}
|
58
|
-
else # compare as hashes
|
59
|
-
tree_compare(new_tree, new_mask, path, index)
|
60
|
-
end
|
61
|
-
else
|
62
|
-
[diff(path, tree, mask.items)]
|
63
|
-
end
|
64
|
-
when Enumerable
|
65
|
-
if tree.is_a?(Enumerable)
|
66
|
-
index ||= 0
|
67
|
-
if !tree.any? && !mask.any?
|
68
|
-
[]
|
69
|
-
elsif !tree.any?
|
70
|
-
[diff(push_path(path, index.to_s), :__missing__, mask)]
|
71
|
-
elsif !mask.any?
|
72
|
-
[diff(push_path(path, index.to_s), tree, :__absent__)]
|
73
|
-
else
|
74
|
-
# if the mask is programmatically generated (unlikely), then
|
75
|
-
# the mask might be really big and this could blow the stack.
|
76
|
-
# don't support this case for now.
|
77
|
-
tree_compare(tree.first, mask.first, push_path(path, index.to_s)) +
|
78
|
-
tree_compare(tree.drop(1), mask.drop(1), path, index + 1)
|
79
|
-
end
|
80
|
-
else
|
81
|
-
[diff(path, tree, mask)]
|
82
|
-
end
|
83
|
-
else
|
84
|
-
tree == mask ? [] : [diff(path, tree, mask)]
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
def hashify_set(items, get_key)
|
92
|
-
Hash[items.map{|x| [get_key.call(x), x] }]
|
93
|
-
end
|
94
|
-
|
95
|
-
def duplicates(xs)
|
96
|
-
xs.group_by{|x| x}.each_pair.select{|_k, v| v.size > 1}.map(&:first)
|
97
|
-
end
|
98
|
-
|
99
|
-
def push_path(path, name)
|
100
|
-
path + [name]
|
101
|
-
end
|
102
|
-
|
103
|
-
def path_string(path)
|
104
|
-
path.join('/')
|
105
|
-
end
|
106
|
-
|
107
|
-
def diff(path, tree, mask)
|
108
|
-
{path: path_string(path), tree: tree, mask: massage_mask_for_diff(mask)}
|
109
|
-
end
|
110
|
-
|
111
|
-
def massage_mask_for_diff(mask)
|
112
|
-
if mask.respond_to?(:call)
|
113
|
-
massaged = :__predicate__
|
114
|
-
begin
|
115
|
-
massaged = mask.to_source
|
116
|
-
rescue Exception => e
|
117
|
-
raise unless e.class.name.start_with?('Sourcify')
|
118
|
-
end
|
119
|
-
massaged
|
120
|
-
else
|
121
|
-
mask
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
public
|
126
|
-
|
127
|
-
def tree_map(h)
|
128
|
-
raise ArgumentError.new('Must provide a transformer block') unless block_given?
|
129
|
-
config = BlockCollector.new
|
130
|
-
yield(config)
|
131
|
-
TransformTreeClosure.new.do_obj(h, config.get_path_tree)
|
132
|
-
end
|
133
|
-
|
134
|
-
class TransformTreeClosure
|
135
|
-
def initialize
|
136
|
-
@bias = 0 # symbol = +, string = -
|
137
|
-
end
|
138
|
-
|
139
|
-
def do_obj(obj, path_tree)
|
140
|
-
case obj
|
141
|
-
when nil; nil
|
142
|
-
when Array; do_array(obj, path_tree)
|
143
|
-
else; do_hash(obj, path_tree)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
private
|
148
|
-
|
149
|
-
def do_hash(h, path_tree)
|
150
|
-
h = h.dup
|
151
|
-
path_tree.each_pair do |name, path_tree|
|
152
|
-
if path_tree.respond_to?(:call)
|
153
|
-
if (hash_name = try_get_hash_name(name))
|
154
|
-
hash_name, old_fh = get_key_and_value(h, hash_name)
|
155
|
-
unless old_fh.nil?
|
156
|
-
h[hash_name] = old_fh.each_with_object(old_fh.dup) do |(key, value), fh|
|
157
|
-
fh[key] = path_tree.call(value.deep_dup)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
elsif (array_name = try_get_array_name(name))
|
161
|
-
array_name, value = get_key_and_value(h, array_name)
|
162
|
-
unless value.nil?
|
163
|
-
h[array_name] = value.map(&:deep_dup).map(&path_tree)
|
164
|
-
end
|
165
|
-
else
|
166
|
-
name, value = get_key_and_value(h, name)
|
167
|
-
h[name] = path_tree.call(value.deep_dup)
|
168
|
-
end
|
169
|
-
else
|
170
|
-
name, value = get_key_and_value(h, name)
|
171
|
-
h[name] = do_obj(value, path_tree)
|
172
|
-
end
|
173
|
-
end
|
174
|
-
h
|
175
|
-
end
|
176
|
-
|
177
|
-
def get_key_and_value(h, string_key)
|
178
|
-
tried_symbol = @bias >= 0
|
179
|
-
trial_key = tried_symbol ? string_key.to_sym : string_key
|
180
|
-
value = h[trial_key]
|
181
|
-
|
182
|
-
if value.nil? # failed
|
183
|
-
@bias += (tried_symbol ? -1 : 1)
|
184
|
-
key = tried_symbol ? string_key : string_key.to_sym
|
185
|
-
[key, h[key]]
|
186
|
-
else
|
187
|
-
@bias += (tried_symbol ? 1 : -1)
|
188
|
-
[trial_key, value]
|
189
|
-
end
|
190
|
-
end
|
191
|
-
|
192
|
-
def do_array(a, path_tree)
|
193
|
-
a.map{|x| do_obj(x, path_tree)}
|
194
|
-
end
|
195
|
-
|
196
|
-
def try_get_hash_name(p)
|
197
|
-
p =~ /(.+)\{\}$/ ? $1 : nil
|
198
|
-
end
|
199
|
-
|
200
|
-
def try_get_array_name(p)
|
201
|
-
p =~ /(.+)\[\]$/ ? $1 : nil
|
202
|
-
end
|
203
|
-
end
|
204
|
-
|
205
|
-
public
|
206
|
-
|
207
|
-
def recursive_delete!(hash, keys)
|
208
|
-
x = hash # hash is named 'hash' for documentation purposes but may be anything
|
209
|
-
case x
|
210
|
-
when Hash
|
211
|
-
keys.each{|k| x.delete(k)}
|
212
|
-
x.each_value{|v| recursive_delete!(v, keys)}
|
213
|
-
when Array
|
214
|
-
x.each{|v| recursive_delete!(v, keys)}
|
215
|
-
end
|
216
|
-
x
|
217
|
-
end
|
218
|
-
|
219
|
-
end
|
220
|
-
end
|
1
|
+
require 'abstractivator/trees/tree_map'
|
2
|
+
require 'abstractivator/trees/tree_compare'
|
3
|
+
require 'abstractivator/trees/recursive_delete'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/core_ext/object/deep_dup'
|
2
|
+
require 'abstractivator/trees/block_collector'
|
3
|
+
require 'sourcify'
|
4
|
+
require 'delegate'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module Abstractivator
|
8
|
+
module Trees
|
9
|
+
def recursive_delete!(hash, keys)
|
10
|
+
x = hash # hash is named 'hash' for documentation purposes but may be anything
|
11
|
+
case x
|
12
|
+
when Hash
|
13
|
+
keys.each{|k| x.delete(k)}
|
14
|
+
x.each_value{|v| recursive_delete!(v, keys)}
|
15
|
+
when Array
|
16
|
+
x.each{|v| recursive_delete!(v, keys)}
|
17
|
+
end
|
18
|
+
x
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'active_support/core_ext/object/deep_dup'
|
2
|
+
require 'abstractivator/trees/block_collector'
|
3
|
+
require 'sourcify'
|
4
|
+
require 'delegate'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module Abstractivator
|
8
|
+
module Trees
|
9
|
+
|
10
|
+
SetMask = Struct.new(:items, :get_key)
|
11
|
+
def set_mask(items, get_key)
|
12
|
+
SetMask.new(items, get_key)
|
13
|
+
end
|
14
|
+
|
15
|
+
def tree_compare(tree, mask, path=[], index=nil)
|
16
|
+
if mask == [:*] && tree.is_a?(Enumerable)
|
17
|
+
[]
|
18
|
+
elsif mask == :+ && tree != :__missing__
|
19
|
+
[]
|
20
|
+
elsif mask == :- && tree != :__missing__
|
21
|
+
[diff(path, tree, :__absent__)]
|
22
|
+
elsif mask.respond_to?(:call)
|
23
|
+
are_equivalent = mask.call(tree)
|
24
|
+
are_equivalent ? [] : [diff(path, tree, mask)]
|
25
|
+
else
|
26
|
+
case mask
|
27
|
+
when Hash
|
28
|
+
if tree.is_a?(Hash)
|
29
|
+
mask.each_pair.flat_map do |k, v|
|
30
|
+
tree_compare(tree.fetch(k, :__missing__), v, push_path(path, k))
|
31
|
+
end
|
32
|
+
else
|
33
|
+
[diff(path, tree, mask)]
|
34
|
+
end
|
35
|
+
when SetMask # must check this before Enumerable because Structs are enumerable
|
36
|
+
if tree.is_a?(Enumerable)
|
37
|
+
# convert the enumerables to hashes, then compare those hashes
|
38
|
+
tree_items = tree
|
39
|
+
mask_items = mask.items.dup
|
40
|
+
get_key = mask.get_key
|
41
|
+
|
42
|
+
be_strict = !mask_items.delete(:*)
|
43
|
+
new_tree = hashify_set(tree_items, get_key)
|
44
|
+
new_mask = hashify_set(mask_items, get_key)
|
45
|
+
tree_keys = Set.new(new_tree.keys)
|
46
|
+
mask_keys = Set.new(new_mask.keys)
|
47
|
+
tree_only = tree_keys - mask_keys
|
48
|
+
|
49
|
+
# report duplicate keys
|
50
|
+
if new_tree.size < tree_items.size
|
51
|
+
diff(path, [:__duplicate_keys__, duplicates(tree_items.map(&get_key))], nil)
|
52
|
+
elsif new_mask.size < mask_items.size
|
53
|
+
diff(path, nil, [:__duplicate_keys__, duplicates(mask_items.map(&get_key))])
|
54
|
+
# hash comparison allows extra values in the tree.
|
55
|
+
# report extra values in the tree unless there was a :* in the mask
|
56
|
+
elsif be_strict && tree_only.any?
|
57
|
+
tree_only.map{|k| diff(push_path(path, k), new_tree[k], :__absent__)}
|
58
|
+
else # compare as hashes
|
59
|
+
tree_compare(new_tree, new_mask, path, index)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
[diff(path, tree, mask.items)]
|
63
|
+
end
|
64
|
+
when Enumerable
|
65
|
+
if tree.is_a?(Enumerable)
|
66
|
+
index ||= 0
|
67
|
+
if !tree.any? && !mask.any?
|
68
|
+
[]
|
69
|
+
elsif !tree.any?
|
70
|
+
[diff(push_path(path, index.to_s), :__missing__, mask)]
|
71
|
+
elsif !mask.any?
|
72
|
+
[diff(push_path(path, index.to_s), tree, :__absent__)]
|
73
|
+
else
|
74
|
+
# if the mask is programmatically generated (unlikely), then
|
75
|
+
# the mask might be really big and this could blow the stack.
|
76
|
+
# don't support this case for now.
|
77
|
+
tree_compare(tree.first, mask.first, push_path(path, index.to_s)) +
|
78
|
+
tree_compare(tree.drop(1), mask.drop(1), path, index + 1)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
[diff(path, tree, mask)]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
tree == mask ? [] : [diff(path, tree, mask)]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def hashify_set(items, get_key)
|
92
|
+
Hash[items.map{|x| [get_key.call(x), x] }]
|
93
|
+
end
|
94
|
+
|
95
|
+
def duplicates(xs)
|
96
|
+
xs.group_by{|x| x}.each_pair.select{|_k, v| v.size > 1}.map(&:first)
|
97
|
+
end
|
98
|
+
|
99
|
+
def push_path(path, name)
|
100
|
+
path + [name]
|
101
|
+
end
|
102
|
+
|
103
|
+
def path_string(path)
|
104
|
+
path.join('/')
|
105
|
+
end
|
106
|
+
|
107
|
+
def diff(path, tree, mask)
|
108
|
+
{path: path_string(path), tree: tree, mask: massage_mask_for_diff(mask)}
|
109
|
+
end
|
110
|
+
|
111
|
+
def massage_mask_for_diff(mask)
|
112
|
+
if mask.respond_to?(:call)
|
113
|
+
massaged = :__predicate__
|
114
|
+
begin
|
115
|
+
massaged = mask.to_source
|
116
|
+
rescue Exception => e
|
117
|
+
raise unless e.class.name.start_with?('Sourcify')
|
118
|
+
end
|
119
|
+
massaged
|
120
|
+
else
|
121
|
+
mask
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'active_support/core_ext/object/deep_dup'
|
2
|
+
require 'abstractivator/trees/block_collector'
|
3
|
+
require 'sourcify'
|
4
|
+
require 'delegate'
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
module Abstractivator
|
8
|
+
module Trees
|
9
|
+
|
10
|
+
# Transforms a tree at certain paths.
|
11
|
+
# The transform is non-destructive and reuses untouched substructure.
|
12
|
+
# For efficiency, it first builds a "path_tree" that describes
|
13
|
+
# which paths to transform. This path_tree is then used as input
|
14
|
+
# for a data-driven algorithm.
|
15
|
+
def tree_map(h)
|
16
|
+
raise ArgumentError.new('Must provide a transformer block') unless block_given?
|
17
|
+
config = BlockCollector.new
|
18
|
+
yield(config)
|
19
|
+
TransformTreeClosure.new(config).do_obj(h, config.get_path_tree)
|
20
|
+
end
|
21
|
+
|
22
|
+
class TransformTreeClosure
|
23
|
+
def initialize(config)
|
24
|
+
@config = config
|
25
|
+
@bias = 0 # symbol = +, string = -
|
26
|
+
@no_value = Object.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def do_obj(obj, path_tree)
|
30
|
+
case obj
|
31
|
+
when nil; nil
|
32
|
+
when Array; do_array(obj, path_tree)
|
33
|
+
else; do_hash(obj, path_tree)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def do_hash(h, path_tree)
|
40
|
+
h = h.dup
|
41
|
+
path_tree.each_pair do |name, path_tree|
|
42
|
+
if leaf?(path_tree)
|
43
|
+
if hash_name = try_get_hash_name(name)
|
44
|
+
hash_name, old_fh = get_key_and_value(h, hash_name)
|
45
|
+
unless old_fh == @no_value || old_fh.nil?
|
46
|
+
old_fh.is_a?(Hash) or raise "Expected a hash but got #{old_fh.class.name}: #{old_fh.inspect}"
|
47
|
+
h[hash_name] = old_fh.each_with_object(old_fh.dup) do |(key, value), fh|
|
48
|
+
replacement = path_tree.call(value.deep_dup, key)
|
49
|
+
if deleted?(replacement)
|
50
|
+
fh.delete(key)
|
51
|
+
else
|
52
|
+
fh[key] = replacement
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
elsif array_name = try_get_array_name(name)
|
57
|
+
array_name, value = get_key_and_value(h, array_name)
|
58
|
+
unless value == @no_value || value.nil?
|
59
|
+
value.is_a?(Array) or raise "Expected an array but got #{value.class.name}: #{value.inspect}"
|
60
|
+
h[array_name] = value.map(&:deep_dup).each_with_index.map(&path_tree).reject(&method(:deleted?))
|
61
|
+
end
|
62
|
+
else
|
63
|
+
name, value = get_key_and_value(h, name)
|
64
|
+
unless value == @no_value
|
65
|
+
replacement = path_tree.call(value.deep_dup)
|
66
|
+
if deleted?(replacement)
|
67
|
+
h.delete(name)
|
68
|
+
else
|
69
|
+
h[name] = replacement
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
else # not leaf
|
74
|
+
name, value = get_key_and_value(h, name)
|
75
|
+
h[name] = do_obj(value, path_tree) unless value == @no_value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
h
|
79
|
+
end
|
80
|
+
|
81
|
+
def leaf?(path_tree)
|
82
|
+
path_tree.respond_to?(:call)
|
83
|
+
end
|
84
|
+
|
85
|
+
def deleted?(value)
|
86
|
+
value == @config.delete
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_key_and_value(h, string_key)
|
90
|
+
tried_symbol = @bias >= 0
|
91
|
+
trial_key = tried_symbol ? string_key.to_sym : string_key
|
92
|
+
value = try_fetch(h, trial_key)
|
93
|
+
|
94
|
+
if value == @no_value # failed
|
95
|
+
@bias += (tried_symbol ? -1 : 1)
|
96
|
+
key = tried_symbol ? string_key : string_key.to_sym
|
97
|
+
[key, try_fetch(h, key)]
|
98
|
+
else
|
99
|
+
@bias += (tried_symbol ? 1 : -1)
|
100
|
+
[trial_key, value]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def try_fetch(h, trial_key)
|
105
|
+
h.fetch(trial_key, @no_value)
|
106
|
+
end
|
107
|
+
|
108
|
+
def do_array(a, path_tree)
|
109
|
+
a.map{|x| do_obj(x, path_tree)}
|
110
|
+
end
|
111
|
+
|
112
|
+
def try_get_hash_name(p)
|
113
|
+
p =~ /(.+)\{\}$/ ? $1 : nil
|
114
|
+
end
|
115
|
+
|
116
|
+
def try_get_array_name(p)
|
117
|
+
p =~ /(.+)\[\]$/ ? $1 : nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'abstractivator/trees/recursive_delete'
|
3
|
+
require 'json'
|
4
|
+
require 'rails'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
describe Abstractivator::Trees do
|
8
|
+
|
9
|
+
include Abstractivator::Trees
|
10
|
+
|
11
|
+
describe '#recursive_delete!' do
|
12
|
+
it 'deletes keys in the root hash' do
|
13
|
+
h = {a: 1, b: 2}
|
14
|
+
recursive_delete!(h, [:a])
|
15
|
+
expect(h).to eql({b: 2})
|
16
|
+
end
|
17
|
+
it 'deletes keys in sub hashes' do
|
18
|
+
h = {a: 1, b: {c: 3, d: 4}}
|
19
|
+
recursive_delete!(h, [:c])
|
20
|
+
expect(h).to eql({a: 1, b: {d: 4}})
|
21
|
+
end
|
22
|
+
it 'deletes keys in hashes inside arrays' do
|
23
|
+
h = {a: [{b: 1, c: 2}, {b: 3, c: 4}]}
|
24
|
+
recursive_delete!(h, [:b])
|
25
|
+
expect(h).to eql({a: [{c: 2}, {c: 4}]})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'rspec'
|
2
|
-
require 'abstractivator/trees'
|
2
|
+
require 'abstractivator/trees/tree_compare'
|
3
3
|
require 'json'
|
4
4
|
require 'rails'
|
5
5
|
require 'pp'
|
@@ -8,161 +8,6 @@ describe Abstractivator::Trees do
|
|
8
8
|
|
9
9
|
include Abstractivator::Trees
|
10
10
|
|
11
|
-
describe '#tree_map' do
|
12
|
-
|
13
|
-
context 'when no block is provided' do
|
14
|
-
it 'raises an exception' do
|
15
|
-
expect{ tree_map(hash) }.to raise_error ArgumentError, 'Must provide a transformer block'
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
it 'handles both string and symbol keys' do
|
20
|
-
h = {:a => 1, 'b' => 2}
|
21
|
-
result = tree_map(h) do |t|
|
22
|
-
t.when('a') {|v| v + 10}
|
23
|
-
t.when('b') {|v| v + 10}
|
24
|
-
end
|
25
|
-
expect(result).to eql({:a => 11, 'b' => 12})
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'replaces primitive-type hash fields' do
|
29
|
-
h = {'a' => 1}
|
30
|
-
result = transform_one_path(h, 'a') { 2 }
|
31
|
-
expect(result).to eql({'a' => 2})
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'replaces nil hash fields' do
|
35
|
-
h = {'a' => nil}
|
36
|
-
result = transform_one_path(h, 'a') {|v| v.to_s}
|
37
|
-
expect(result).to eql({'a' => ''})
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'replaces hash-type hash fields' do
|
41
|
-
h = {'a' => {'b' => 1}}
|
42
|
-
result = transform_one_path(h, 'a') { {'z' => 99} }
|
43
|
-
expect(result).to eql({'a' => {'z' => 99}})
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'replaces array-type hash fields' do
|
47
|
-
h = {'a' => [1,2,3]}
|
48
|
-
result = transform_one_path(h, 'a') {|v| v.reverse}
|
49
|
-
expect(result).to eql({'a' => [3,2,1]})
|
50
|
-
end
|
51
|
-
|
52
|
-
it 'replaces primitive-type hash members' do
|
53
|
-
h = {'a' => {'b' => 'foo', 'c' => 'bar'}}
|
54
|
-
result = transform_one_path(h, 'a{}') {|v| v.reverse}
|
55
|
-
expect(result).to eql({'a' => {'b' => 'oof', 'c' => 'rab'}})
|
56
|
-
end
|
57
|
-
|
58
|
-
it 'replaces hash-type hash members' do
|
59
|
-
h = {'a' => {'b' => {'x' => 88}, 'c' => {'x' => 88}}}
|
60
|
-
result = transform_one_path(h, 'a{}') {|v| {'y' => 99}}
|
61
|
-
expect(result).to eql({'a' => {'b' => {'y' => 99}, 'c' => {'y' => 99}}})
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'replaces array-type hash members' do
|
65
|
-
h = {'a' => {'b' => [1,2,3], 'c' => [4,5,6]}}
|
66
|
-
result = transform_one_path(h, 'a{}') {|v| v.reverse}
|
67
|
-
expect(result).to eql({'a' => {'b' => [3,2,1], 'c' => [6,5,4]}})
|
68
|
-
end
|
69
|
-
|
70
|
-
it 'replaces primitive-type array members' do
|
71
|
-
h = {'a' => [1, 2]}
|
72
|
-
result = transform_one_path(h, 'a[]') {|v| v + 10}
|
73
|
-
expect(result).to eql({'a' => [11, 12]})
|
74
|
-
end
|
75
|
-
|
76
|
-
it 'replaces hash-type array members' do
|
77
|
-
h = {'a' => [{'b' => 1}, {'c' => 2}]}
|
78
|
-
result = transform_one_path(h, 'a[]') { {'z' => 99} }
|
79
|
-
expect(result).to eql({'a' => [{'z' => 99}, {'z' => 99}]})
|
80
|
-
end
|
81
|
-
|
82
|
-
it 'replaces array-type array members' do
|
83
|
-
h = {'a' => [[1,2,3], [4,5,6]]}
|
84
|
-
result = transform_one_path(h, 'a[]') {|v| v.reverse}
|
85
|
-
expect(result).to eql({'a' => [[3,2,1], [6,5,4]]})
|
86
|
-
end
|
87
|
-
|
88
|
-
context 'when replacing array members' do
|
89
|
-
it 'allows the array to be nil' do
|
90
|
-
h = {'a' => nil}
|
91
|
-
result = transform_one_path(h, 'a[]') {|v| v + 1}
|
92
|
-
expect(result).to eql({'a' => nil})
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
context 'when replacing hash members' do
|
97
|
-
it 'allows the hash to be nil' do
|
98
|
-
h = {'a' => nil}
|
99
|
-
result = transform_one_path(h, 'a{}') {|v| v + 1}
|
100
|
-
expect(result).to eql({'a' => nil})
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
context 'mutation' do
|
105
|
-
before(:each) do
|
106
|
-
@old = {'a' => {'x' => 1, 'y' => 2}, 'b' => {'x' => 17, 'y' => 23}}
|
107
|
-
@new = transform_one_path(@old,'a') {|v|
|
108
|
-
v['z'] = v['x'] + v['y']
|
109
|
-
v
|
110
|
-
}
|
111
|
-
end
|
112
|
-
it 'does not mutate the input' do
|
113
|
-
expect(@old).to eql({'a' => {'x' => 1, 'y' => 2}, 'b' => {'x' => 17, 'y' => 23}})
|
114
|
-
expect(@new).to eql({'a' => {'x' => 1, 'y' => 2, 'z' => 3}, 'b' => {'x' => 17, 'y' => 23}})
|
115
|
-
end
|
116
|
-
it 'preserves unmodified substructure' do
|
117
|
-
expect(@old['a'].equal?(@new['a'])).to be_falsey
|
118
|
-
expect(@old['b'].equal?(@new['b'])).to be_truthy
|
119
|
-
end
|
120
|
-
|
121
|
-
#TODO: create a generic json file to use for this test
|
122
|
-
# it 'really does not mutate the input' do
|
123
|
-
# old = JSON.parse(File.read('assay.json'))
|
124
|
-
# old2 = old.deep_dup
|
125
|
-
# tree_map(old) do |t|
|
126
|
-
# t.when('compound_methods/calibration/normalizers[]') {|v| v.to_s.reverse}
|
127
|
-
# t.when('compound_methods/calibration/responses[]') {|v| v.to_s.reverse}
|
128
|
-
# t.when('compound_methods/rule_settings{}') {|v| v.to_s.reverse}
|
129
|
-
# t.when('compound_methods/chromatogram_methods/rule_settings{}') {|v| v.to_s.reverse}
|
130
|
-
# t.when('compound_methods/chromatogram_methods/peak_integration/retention_time') do |ret_time|
|
131
|
-
# if ret_time['reference_type_source'] == 'chromatogram'
|
132
|
-
# ret_time['reference'] = ret_time['reference'].to_s.reverse
|
133
|
-
# end
|
134
|
-
# ret_time
|
135
|
-
# end
|
136
|
-
# end
|
137
|
-
# expect(old).to eql old2
|
138
|
-
# end
|
139
|
-
end
|
140
|
-
|
141
|
-
def transform_one_path(h, path, &block)
|
142
|
-
tree_map(h) do |t|
|
143
|
-
t.when(path, &block)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
describe '#recursive_delete!' do
|
149
|
-
it 'deletes keys in the root hash' do
|
150
|
-
h = {a: 1, b: 2}
|
151
|
-
recursive_delete!(h, [:a])
|
152
|
-
expect(h).to eql({b: 2})
|
153
|
-
end
|
154
|
-
it 'deletes keys in sub hashes' do
|
155
|
-
h = {a: 1, b: {c: 3, d: 4}}
|
156
|
-
recursive_delete!(h, [:c])
|
157
|
-
expect(h).to eql({a: 1, b: {d: 4}})
|
158
|
-
end
|
159
|
-
it 'deletes keys in hashes inside arrays' do
|
160
|
-
h = {a: [{b: 1, c: 2}, {b: 3, c: 4}]}
|
161
|
-
recursive_delete!(h, [:b])
|
162
|
-
expect(h).to eql({a: [{c: 2}, {c: 4}]})
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
11
|
describe '#tree_compare' do
|
167
12
|
|
168
13
|
extend Abstractivator::Trees
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'abstractivator/trees/tree_map'
|
3
|
+
require 'json'
|
4
|
+
require 'rails'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
describe Abstractivator::Trees do
|
8
|
+
|
9
|
+
include Abstractivator::Trees
|
10
|
+
|
11
|
+
describe '#tree_map' do
|
12
|
+
|
13
|
+
context 'when no block is provided' do
|
14
|
+
it 'raises an exception' do
|
15
|
+
expect{ tree_map(hash) }.to raise_error ArgumentError, 'Must provide a transformer block'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'handles both string and symbol keys' do
|
20
|
+
h = {:a => 1, 'b' => 2}
|
21
|
+
result = tree_map(h) do |t|
|
22
|
+
t.when('a') {|v| v + 10}
|
23
|
+
t.when('b') {|v| v + 10}
|
24
|
+
end
|
25
|
+
expect(result).to eql({:a => 11, 'b' => 12})
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'replaces primitive-type hash fields' do
|
29
|
+
h = {'a' => 1}
|
30
|
+
result = transform_one_path(h, 'a') { 2 }
|
31
|
+
expect(result).to eql({'a' => 2})
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'replaces nil hash fields' do
|
35
|
+
h = {'a' => nil}
|
36
|
+
result = transform_one_path(h, 'a') {|v| v.to_s}
|
37
|
+
expect(result).to eql({'a' => ''})
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'replaces hash-type hash fields' do
|
41
|
+
h = {'a' => {'b' => 1}}
|
42
|
+
result = transform_one_path(h, 'a') { {'z' => 99} }
|
43
|
+
expect(result).to eql({'a' => {'z' => 99}})
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'replaces array-type hash fields' do
|
47
|
+
h = {'a' => [1,2,3]}
|
48
|
+
result = transform_one_path(h, 'a') {|v| v.reverse}
|
49
|
+
expect(result).to eql({'a' => [3,2,1]})
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'replaces primitive-type hash members' do
|
53
|
+
h = {'a' => {'b' => 'foo', 'c' => 'bar'}}
|
54
|
+
result = transform_one_path(h, 'a{}') {|v| v.reverse}
|
55
|
+
expect(result).to eql({'a' => {'b' => 'oof', 'c' => 'rab'}})
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'replaces hash-type hash members' do
|
59
|
+
h = {'a' => {'b' => {'x' => 88}, 'c' => {'x' => 88}}}
|
60
|
+
result = transform_one_path(h, 'a{}') {|v| {'y' => 99}}
|
61
|
+
expect(result).to eql({'a' => {'b' => {'y' => 99}, 'c' => {'y' => 99}}})
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'replaces array-type hash members' do
|
65
|
+
h = {'a' => {'b' => [1,2,3], 'c' => [4,5,6]}}
|
66
|
+
result = transform_one_path(h, 'a{}') {|v| v.reverse}
|
67
|
+
expect(result).to eql({'a' => {'b' => [3,2,1], 'c' => [6,5,4]}})
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'replaces primitive-type array members' do
|
71
|
+
h = {'a' => [1, 2]}
|
72
|
+
result = transform_one_path(h, 'a[]') {|v| v + 10}
|
73
|
+
expect(result).to eql({'a' => [11, 12]})
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'replaces hash-type array members' do
|
77
|
+
h = {'a' => [{'b' => 1}, {'c' => 2}]}
|
78
|
+
result = transform_one_path(h, 'a[]') { {'z' => 99} }
|
79
|
+
expect(result).to eql({'a' => [{'z' => 99}, {'z' => 99}]})
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'replaces array-type array members' do
|
83
|
+
h = {'a' => [[1,2,3], [4,5,6]]}
|
84
|
+
result = transform_one_path(h, 'a[]') {|v| v.reverse}
|
85
|
+
expect(result).to eql({'a' => [[3,2,1], [6,5,4]]})
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'deletes values' do
|
89
|
+
h = {
|
90
|
+
a: 1,
|
91
|
+
b: [4, 5, 6],
|
92
|
+
c: { d: 9, e: 10 },
|
93
|
+
f: { g: 101, h: 102, i: 103 }
|
94
|
+
}
|
95
|
+
result = tree_map(h) do |t|
|
96
|
+
t.when('a') { t.delete }
|
97
|
+
t.when('c/d') { t.delete }
|
98
|
+
t.when('b[]') { |x| x.even? ? t.delete : x }
|
99
|
+
t.when('f{}') { |x| x.even? ? t.delete : x }
|
100
|
+
end
|
101
|
+
expect(result).to eql({b: [5], c: {e: 10}, f: {g: 101, i: 103}})
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'when replacing array members' do
|
105
|
+
it 'allows the array to be nil' do
|
106
|
+
h = {'a' => nil}
|
107
|
+
result = transform_one_path(h, 'a[]') {|v| v + 1}
|
108
|
+
expect(result).to eql({'a' => nil})
|
109
|
+
end
|
110
|
+
it 'passes the index as the second argument' do
|
111
|
+
h = {a: %w(one two)}
|
112
|
+
call_count = 0
|
113
|
+
transform_one_path(h, 'a[]') do |v, i|
|
114
|
+
expect(i).to eql 0 if v == 'one'
|
115
|
+
expect(i).to eql 1 if v == 'two'
|
116
|
+
call_count += 1
|
117
|
+
end
|
118
|
+
expect(call_count).to eql 2
|
119
|
+
end
|
120
|
+
it 'raises an error is the value is not an array' do
|
121
|
+
expect{transform_one_path({a: 1}, 'a[]') { |x| x }}.to raise_error 'Expected an array but got Fixnum: 1'
|
122
|
+
expect{transform_one_path({a: {b: 1}}, 'a[]') { |x| x }}.to raise_error 'Expected an array but got Hash: {:b=>1}'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
context 'when replacing hash members' do
|
127
|
+
it 'allows the hash to be nil' do
|
128
|
+
h = {'a' => nil}
|
129
|
+
result = transform_one_path(h, 'a{}') {|v| v + 1}
|
130
|
+
expect(result).to eql({'a' => nil})
|
131
|
+
end
|
132
|
+
it 'passes the key as the second argument' do
|
133
|
+
h = {a: {b: 1, c: 2}}
|
134
|
+
call_count = 0
|
135
|
+
transform_one_path(h, 'a{}') do |v, k|
|
136
|
+
expect(k).to eql :b if v == 1
|
137
|
+
expect(k).to eql :c if v == 2
|
138
|
+
call_count += 1
|
139
|
+
end
|
140
|
+
expect(call_count).to eql 2
|
141
|
+
end
|
142
|
+
it 'raises an error is the value is not a hash' do
|
143
|
+
expect{transform_one_path({a: 1}, 'a{}') { |x| x }}.to raise_error 'Expected a hash but got Fixnum: 1'
|
144
|
+
expect{transform_one_path({a: [1, 2]}, 'a{}') { |x| x }}.to raise_error 'Expected a hash but got Array: [1, 2]'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'it does not add missing keys' do # regression test
|
149
|
+
result = tree_map({}) do |t|
|
150
|
+
t.when('foo') { |x| x }
|
151
|
+
t.when('bars[]') { |x| x }
|
152
|
+
t.when('others/baz') { |x| x }
|
153
|
+
end
|
154
|
+
expect(result).to eql({})
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'mutation' do
|
158
|
+
before(:each) do
|
159
|
+
@old = {'a' => {'x' => 1, 'y' => 2}, 'b' => {'x' => 17, 'y' => 23}}
|
160
|
+
@new = transform_one_path(@old,'a') {|v|
|
161
|
+
v['z'] = v['x'] + v['y']
|
162
|
+
v
|
163
|
+
}
|
164
|
+
end
|
165
|
+
it 'does not mutate the input' do
|
166
|
+
expect(@old).to eql({'a' => {'x' => 1, 'y' => 2}, 'b' => {'x' => 17, 'y' => 23}})
|
167
|
+
expect(@new).to eql({'a' => {'x' => 1, 'y' => 2, 'z' => 3}, 'b' => {'x' => 17, 'y' => 23}})
|
168
|
+
end
|
169
|
+
it 'preserves unmodified substructure' do
|
170
|
+
expect(@old['a'].equal?(@new['a'])).to be_falsey
|
171
|
+
expect(@old['b'].equal?(@new['b'])).to be_truthy
|
172
|
+
end
|
173
|
+
|
174
|
+
#TODO: create a generic json file to use for this test
|
175
|
+
# it 'really does not mutate the input' do
|
176
|
+
# old = JSON.parse(File.read('assay.json'))
|
177
|
+
# old2 = old.deep_dup
|
178
|
+
# tree_map(old) do |t|
|
179
|
+
# t.when('compound_methods/calibration/normalizers[]') {|v| v.to_s.reverse}
|
180
|
+
# t.when('compound_methods/calibration/responses[]') {|v| v.to_s.reverse}
|
181
|
+
# t.when('compound_methods/rule_settings{}') {|v| v.to_s.reverse}
|
182
|
+
# t.when('compound_methods/chromatogram_methods/rule_settings{}') {|v| v.to_s.reverse}
|
183
|
+
# t.when('compound_methods/chromatogram_methods/peak_integration/retention_time') do |ret_time|
|
184
|
+
# if ret_time['reference_type_source'] == 'chromatogram'
|
185
|
+
# ret_time['reference'] = ret_time['reference'].to_s.reverse
|
186
|
+
# end
|
187
|
+
# ret_time
|
188
|
+
# end
|
189
|
+
# end
|
190
|
+
# expect(old).to eql old2
|
191
|
+
# end
|
192
|
+
end
|
193
|
+
|
194
|
+
def transform_one_path(h, path, &block)
|
195
|
+
tree_map(h) do |t|
|
196
|
+
t.when(path, &block)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abstractivator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.23
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Winton
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -89,6 +89,9 @@ files:
|
|
89
89
|
- lib/abstractivator/proc_ext.rb
|
90
90
|
- lib/abstractivator/trees.rb
|
91
91
|
- lib/abstractivator/trees/block_collector.rb
|
92
|
+
- lib/abstractivator/trees/recursive_delete.rb
|
93
|
+
- lib/abstractivator/trees/tree_compare.rb
|
94
|
+
- lib/abstractivator/trees/tree_map.rb
|
92
95
|
- lib/abstractivator/version.rb
|
93
96
|
- lib/enumerable_ext.rb
|
94
97
|
- spec/lib/abstractivator/array_ext_spec.rb
|
@@ -97,7 +100,9 @@ files:
|
|
97
100
|
- spec/lib/abstractivator/enum_spec.rb
|
98
101
|
- spec/lib/abstractivator/enumerable_ext_spec.rb
|
99
102
|
- spec/lib/abstractivator/proc_ext_spec.rb
|
100
|
-
- spec/lib/abstractivator/
|
103
|
+
- spec/lib/abstractivator/trees/recursive_delete_spec.rb
|
104
|
+
- spec/lib/abstractivator/trees/tree_compare_spec.rb
|
105
|
+
- spec/lib/abstractivator/trees/tree_map_spec.rb
|
101
106
|
- spec/lib/enumerable_ext_spec.rb
|
102
107
|
homepage: ''
|
103
108
|
licenses:
|
@@ -130,6 +135,8 @@ test_files:
|
|
130
135
|
- spec/lib/abstractivator/enum_spec.rb
|
131
136
|
- spec/lib/abstractivator/enumerable_ext_spec.rb
|
132
137
|
- spec/lib/abstractivator/proc_ext_spec.rb
|
133
|
-
- spec/lib/abstractivator/
|
138
|
+
- spec/lib/abstractivator/trees/recursive_delete_spec.rb
|
139
|
+
- spec/lib/abstractivator/trees/tree_compare_spec.rb
|
140
|
+
- spec/lib/abstractivator/trees/tree_map_spec.rb
|
134
141
|
- spec/lib/enumerable_ext_spec.rb
|
135
142
|
has_rdoc:
|