abstractivator 0.0.15
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 +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/abstractivator.gemspec +25 -0
- data/lib/abstractivator.rb +2 -0
- data/lib/abstractivator/collections.rb +22 -0
- data/lib/abstractivator/cons.rb +58 -0
- data/lib/abstractivator/enumerable_ext.rb +94 -0
- data/lib/abstractivator/proc_ext.rb +52 -0
- data/lib/abstractivator/trees.rb +220 -0
- data/lib/abstractivator/trees/block_collector.rb +34 -0
- data/lib/abstractivator/version.rb +3 -0
- data/lib/enumerable_ext.rb +11 -0
- data/spec/lib/abstractivator/collections_spec.rb +20 -0
- data/spec/lib/abstractivator/cons_spec.rb +48 -0
- data/spec/lib/abstractivator/enumerable_ext_spec.rb +144 -0
- data/spec/lib/abstractivator/proc_ext_spec.rb +76 -0
- data/spec/lib/abstractivator/tree_visitor_spec.rb +320 -0
- data/spec/lib/enumerable_ext_spec.rb +20 -0
- metadata +129 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module Abstractivator
|
2
|
+
module Trees
|
3
|
+
|
4
|
+
class BlockCollector
|
5
|
+
def initialize
|
6
|
+
@config = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def when(path, &block)
|
10
|
+
@config[path] = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_path_tree
|
14
|
+
path_tree = {}
|
15
|
+
@config.each_pair do |path, block|
|
16
|
+
set_hash_path(path_tree, path.split('/'), block)
|
17
|
+
end
|
18
|
+
path_tree
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def set_hash_path(h, names, block)
|
24
|
+
orig = h
|
25
|
+
while names.size > 1
|
26
|
+
h = (h[names.shift] ||= {})
|
27
|
+
end
|
28
|
+
h[names.shift] = block
|
29
|
+
orig
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Enumerable
|
2
|
+
def stable_sort(&compare)
|
3
|
+
compare = compare || ->(a, b){a <=> b}
|
4
|
+
xis = self.each_with_index.map{|x, i| [x, i]}
|
5
|
+
sorted = xis.sort do |(a, ai), (b, bi)|
|
6
|
+
primary = compare.call(a, b)
|
7
|
+
primary != 0 ? primary : (ai <=> bi)
|
8
|
+
end
|
9
|
+
sorted.map(&:first)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'abstractivator/collections'
|
3
|
+
|
4
|
+
describe Abstractivator::Collections do
|
5
|
+
|
6
|
+
include Abstractivator::Collections
|
7
|
+
|
8
|
+
describe '#multizip' do
|
9
|
+
it 'transposes' do
|
10
|
+
xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
11
|
+
expect(multizip(xs)).to eql [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
|
12
|
+
expect(multizip([])).to eql []
|
13
|
+
end
|
14
|
+
it 'uses a default value past the end of shorter enumerables' do
|
15
|
+
xs = [[1, 2, 3], [4], [7, 8, 9]]
|
16
|
+
expect(multizip(xs)).to eql [[1, 4, 7], [2, nil, 8], [3, nil, 9]]
|
17
|
+
expect(multizip(xs, -1)).to eql [[1, 4, 7], [2, -1, 8], [3, -1, 9]]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'abstractivator/cons'
|
3
|
+
|
4
|
+
describe Abstractivator::Cons do
|
5
|
+
|
6
|
+
include Abstractivator::Cons
|
7
|
+
|
8
|
+
describe '#empty_list' do
|
9
|
+
it 'is a singleton' do
|
10
|
+
expect(empty_list).to eql empty_list
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#cons' do
|
15
|
+
it 'creates a cons cell' do
|
16
|
+
expect(cons(1, 2)).to eql [1,2 ]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#head' do
|
21
|
+
it 'returns the head' do
|
22
|
+
cell = cons(1, 2)
|
23
|
+
expect(cell.head).to eql 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#tail' do
|
28
|
+
it 'returns the tail' do
|
29
|
+
cell = cons(1, 2)
|
30
|
+
expect(cell.tail).to eql 2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#enum_to_list' do
|
35
|
+
it 'returns the list form of an enumerable' do
|
36
|
+
expect(enum_to_list([])).to eql empty_list
|
37
|
+
expect(enum_to_list([1,2,3])).to eql [1, [2, [3, empty_list]]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#list_to_enum' do
|
42
|
+
it 'returns the enumerable form of a list' do
|
43
|
+
expect(list_to_enum(empty_list).to_a).to eql []
|
44
|
+
expect(list_to_enum(cons(1, cons(2, cons(3, empty_list)))).to_a).to eql [1, 2, 3]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'abstractivator/enumerable_ext'
|
2
|
+
|
3
|
+
describe Enumerable do
|
4
|
+
let!(:x) {[
|
5
|
+
{ a: 5, b: 6, c: 'asdf' },
|
6
|
+
{ a: 8, b: 5, c: 'ffffsfsf' },
|
7
|
+
{ a: 3, b: 2, c: 'rwrwrwr' }
|
8
|
+
]}
|
9
|
+
|
10
|
+
let!(:y) {[
|
11
|
+
{ a: 9, b: 10, c: 'aaaaaarghj' },
|
12
|
+
{ a: 3, b: 2, c: 'ggggggg' },
|
13
|
+
{ a: 5, b: 6, c: 'rrrrrrrrrr' }
|
14
|
+
]}
|
15
|
+
let!(:get_key) { ->(z) { [z[:a], z[:b]] } }
|
16
|
+
|
17
|
+
describe '::inner_join' do
|
18
|
+
it 'returns only the matched values' do
|
19
|
+
result = Enumerable.inner_join(x, y, get_key, get_key)
|
20
|
+
expect(result.size).to eql 2
|
21
|
+
expect(result).to include([{ a: 5, b: 6, c: 'asdf' }, { a: 5, b: 6, c: 'rrrrrrrrrr' }])
|
22
|
+
expect(result).to include([{ a: 3, b: 2, c: 'rwrwrwr' }, { a: 3, b: 2, c: 'ggggggg' }])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '::outer_join' do
|
27
|
+
it 'matches up elements by key' do
|
28
|
+
result = Enumerable.outer_join(x, y, get_key, get_key, -1, -100)
|
29
|
+
expect(result).to include([{ a: 5, b: 6, c: 'asdf' }, { a: 5, b: 6, c: 'rrrrrrrrrr' }])
|
30
|
+
expect(result).to include([{ a: 3, b: 2, c: 'rwrwrwr' }, { a: 3, b: 2, c: 'ggggggg' }])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'matches a left value up with the right default when the right is missing' do
|
34
|
+
result = Enumerable.outer_join(x, y, get_key, get_key, -1, -100)
|
35
|
+
expect(result).to include([{ a: 8, b: 5, c: 'ffffsfsf' }, -100])
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'matches a right value up with the left default when the left is missing' do
|
39
|
+
result = Enumerable.outer_join(x, y, get_key, get_key, -1, -100)
|
40
|
+
expect(result).to include([-1, { a: 9, b: 10, c: 'aaaaaarghj' }])
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'invokes default value procs' do
|
44
|
+
result = Enumerable.outer_join(x, y, get_key, get_key, ->(x){x[:c]}, ->(x){x[:c]})
|
45
|
+
expect(result).to include([{ a: 8, b: 5, c: 'ffffsfsf' }, 'ffffsfsf'])
|
46
|
+
expect(result).to include(['aaaaaarghj', { a: 9, b: 10, c: 'aaaaaarghj' }])
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns the correct number of pairs' do
|
50
|
+
result = Enumerable.outer_join(x, y, get_key, get_key, -1, -100)
|
51
|
+
expect(result.size).to eql 4
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'works when left is empty' do
|
55
|
+
result = Enumerable.outer_join([], y, get_key, get_key, -1, -100)
|
56
|
+
expect(result.size).to eql 3
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'works when right is empty' do
|
60
|
+
result = Enumerable.outer_join(x, [], get_key, get_key, -1, -100)
|
61
|
+
expect(result.size).to eql 3
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'works when both are empty' do
|
65
|
+
result = Enumerable.outer_join([], [], get_key, get_key, -1, -100)
|
66
|
+
expect(result.size).to eql 0
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'throws an exception when left values have overlapping keys' do
|
70
|
+
x.push({ a: 8, b: 5, c: 'oops' })
|
71
|
+
expect { Enumerable.outer_join(x, y, get_key, get_key, -1, -100) }.to raise_error
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'throws an exception when right values have overlapping keys' do
|
75
|
+
y.push({ a: 5, b: 6, c: 'oops' })
|
76
|
+
expect { Enumerable.outer_join(x, y, get_key, get_key, -1, -100) }.to raise_error
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#uniq?' do
|
81
|
+
it 'returns true if the items are unique' do
|
82
|
+
expect([1,2,3].uniq?).to be true
|
83
|
+
end
|
84
|
+
it 'returns false if the items are unique' do
|
85
|
+
expect([1,2,2].uniq?).to be false
|
86
|
+
end
|
87
|
+
it 'accepts a block' do
|
88
|
+
expect([[1, 99], [2, 99]].uniq?(&:first)).to be true
|
89
|
+
expect([[1, 99], [2, 99]].uniq?(&:last)).to be false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#detect' do
|
94
|
+
let!(:xs) { [Array, Hash] }
|
95
|
+
it 'falls back to the default behavior' do
|
96
|
+
expect(xs.detect{|x| x.name == 'Hash'}).to eql Hash
|
97
|
+
end
|
98
|
+
it 'falls back to the default behavior' do
|
99
|
+
expect(xs.detect(proc{'123'}){|x| x.name == 'Object'}).to eql '123'
|
100
|
+
end
|
101
|
+
context 'with attr_name and value' do
|
102
|
+
it 'returns the matching element' do
|
103
|
+
expect(xs.detect(:name, 'Hash')).to eql Hash
|
104
|
+
end
|
105
|
+
it 'returns nil if not found' do
|
106
|
+
expect([].detect(:name, 'Hash')).to be_nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
context 'with value and block' do
|
110
|
+
it 'returns the matching element' do
|
111
|
+
expect(xs.detect('Hash', &:name)).to eql Hash
|
112
|
+
end
|
113
|
+
it 'returns the matching element' do
|
114
|
+
expect([].detect('Hash', &:name)).to be_nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#inject_right' do
|
120
|
+
it 'injects, starting at the right' do
|
121
|
+
expect([1, 2, 3].inject_right([]){|acc, x| acc << x}).to eql [3, 2, 1]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#pad_right' do
|
126
|
+
it 'pads values at the end' do
|
127
|
+
a = [1, 2]
|
128
|
+
expect(a.pad_right(4, :x)).to eql [1, 2, :x, :x]
|
129
|
+
expect(a.pad_right(2, :x)).to eql [1, 2]
|
130
|
+
expect(a.pad_right(1, :x)).to eql [1, 2]
|
131
|
+
expect(a.pad_right(-1, :x)).to eql [1, 2]
|
132
|
+
end
|
133
|
+
it 'does not mutate the receiver' do
|
134
|
+
a = [1, 2]
|
135
|
+
a.pad_right(4, :x)
|
136
|
+
expect(a).to eql [1, 2]
|
137
|
+
end
|
138
|
+
it 'accepts a block to create values' do
|
139
|
+
n = 0
|
140
|
+
result = [:x].pad_right(4) { n += 1 }
|
141
|
+
expect(result).to eql [:x, 1, 2, 3]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'abstractivator/proc_ext'
|
2
|
+
|
3
|
+
context 'in the world of functional programming' do
|
4
|
+
let!(:double) { proc{|x| x * 2} }
|
5
|
+
let!(:square) { proc{|x| x ** 2} }
|
6
|
+
let!(:negate) { proc{|x| -x} }
|
7
|
+
|
8
|
+
describe 'Proc#compose' do
|
9
|
+
it 'composes procs' do
|
10
|
+
expect(double.compose(square).call(3)).to eql 18
|
11
|
+
expect(square.compose(double).call(3)).to eql 36
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'Proc::compose' do
|
16
|
+
it 'composes procs' do
|
17
|
+
expect(Proc.compose.call(3)).to eql 3
|
18
|
+
expect(Proc.compose(double).call(3)).to eql 6
|
19
|
+
expect(Proc.compose(square, double).call(3)).to eql 36
|
20
|
+
expect(Proc.compose(negate, square, double).call(3)).to eql -36
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'Proc#reverse_args' do
|
25
|
+
it 'reverse argument order' do
|
26
|
+
divide = proc {|a, b| a / b}
|
27
|
+
expect(divide.reverse_args.call(4.0, 1.0)).to eql 0.25
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'Proc::loose_call' do
|
32
|
+
it 'returns the first argument if it is not a proc' do
|
33
|
+
expect(Proc.loose_call(:a, [:b, :c])).to eql :a
|
34
|
+
end
|
35
|
+
it 'calls the proc with an appropriate number of arguments' do
|
36
|
+
events = []
|
37
|
+
args = [:here, :are, :some, :arguments]
|
38
|
+
Proc.loose_call(->{events << 0}, args)
|
39
|
+
Proc.loose_call(->(a){events << 1}, args)
|
40
|
+
Proc.loose_call(->(a, b){events << 2}, args)
|
41
|
+
Proc.loose_call(->(a, b, c){events << 3}, args)
|
42
|
+
expect(events).to eql [0, 1, 2, 3]
|
43
|
+
end
|
44
|
+
it 'pads with nils' do
|
45
|
+
expect(Proc.loose_call(->(a, b) {[a, b]}, [1])).to eql [1, nil]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'Proc#loosen_args' do
|
50
|
+
it 'returns a procedure with loose arity semantics' do
|
51
|
+
p = ->(a, b, c) { [a, b, c] }
|
52
|
+
lp = p.loosen_args
|
53
|
+
expect(lp.call(1, 2)).to eql [1, 2, nil]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'UnboundMethod#explicit_receiver' do
|
59
|
+
it 'returns a proc that takes an explicit self to bind to as the first argument' do
|
60
|
+
m = Array.instance_method(:<<)
|
61
|
+
a = []
|
62
|
+
m.explicit_receiver.call(a, 42)
|
63
|
+
expect(a).to eql [42]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'Array#to_proc' do
|
68
|
+
it 'makes a hash-accessor proc' do
|
69
|
+
expect([{a: 1, b: 2}, {a: 3, b: 3}].map(&[:a])).to eql [1, 3]
|
70
|
+
expect([{'a' => 1, 'b' => 2}, {'a' => 3, 'b' => 3}].map(&['a'])).to eql [1, 3]
|
71
|
+
end
|
72
|
+
it 'raises an error if you use it wrong' do
|
73
|
+
expect{[].to_proc}.to raise_error 'size must be exactly one'
|
74
|
+
expect{[:a, :b].to_proc}.to raise_error 'size must be exactly one'
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'abstractivator/trees'
|
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
|
+
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
|
+
describe '#tree_compare' do
|
167
|
+
|
168
|
+
extend Abstractivator::Trees
|
169
|
+
|
170
|
+
def self.example(description, values)
|
171
|
+
it description do
|
172
|
+
tree, mask, expected = values[:tree], values[:mask], values[:result]
|
173
|
+
expect(tree_compare(tree, mask)).to eql expected
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
example 'returns an empty list if the tree is comparable to the mask',
|
178
|
+
tree: {a: 1},
|
179
|
+
mask: {a: 1},
|
180
|
+
result: []
|
181
|
+
|
182
|
+
example 'only requires the mask to match a subtree',
|
183
|
+
tree: {a: 1, b: 1},
|
184
|
+
mask: {a: 1},
|
185
|
+
result: []
|
186
|
+
|
187
|
+
example 'returns a list of differences',
|
188
|
+
tree: {a: 1, b: {c: [8, 8]}},
|
189
|
+
mask: {a: 2, b: {c: [8, 9]}},
|
190
|
+
result: [{path: 'a', tree: 1, mask: 2},
|
191
|
+
{path: 'b/c/1', tree: 8, mask: 9}]
|
192
|
+
|
193
|
+
example 'returns a list of differences for missing values',
|
194
|
+
tree: {},
|
195
|
+
mask: {a: 2, b: nil},
|
196
|
+
result: [{path: 'a', tree: :__missing__, mask: 2}, {path: 'b', tree: :__missing__, mask: nil}]
|
197
|
+
|
198
|
+
example 'compares hash values',
|
199
|
+
tree: {a: 1},
|
200
|
+
mask: {a: 2},
|
201
|
+
result: [{path: 'a', tree: 1, mask: 2}]
|
202
|
+
|
203
|
+
example 'compares array values',
|
204
|
+
tree: {a: [1, 2]},
|
205
|
+
mask: {a: [1, 3]},
|
206
|
+
result: [{path: 'a/1', tree: 2, mask: 3}]
|
207
|
+
|
208
|
+
example 'compares with predicates',
|
209
|
+
tree: {a: 1},
|
210
|
+
mask: {a: proc {|x| x.even?}},
|
211
|
+
result: [{path: 'a', tree: 1, mask: 'proc { |x| x.even? }'}]
|
212
|
+
|
213
|
+
example 'compares with predicates (degrades gracefully when source code is unavailable)',
|
214
|
+
tree: {a: 1},
|
215
|
+
mask: {a: :even?.to_proc},
|
216
|
+
result: [{path: 'a', tree: 1, mask: :__predicate__}]
|
217
|
+
|
218
|
+
it 'compares with predicates (lets non-sourcify errors through)' do
|
219
|
+
expect{tree_compare({a: 1}, {a: proc { raise 'oops' }})}.to raise_error
|
220
|
+
end
|
221
|
+
|
222
|
+
example 'can ensure values are absent with :-',
|
223
|
+
tree: {a: 1},
|
224
|
+
mask: {a: :-},
|
225
|
+
result: [{path: 'a', tree: 1, mask: :__absent__}]
|
226
|
+
|
227
|
+
example 'can check for any value being present with :+',
|
228
|
+
tree: {a: 1, b: [1, 2, 3]},
|
229
|
+
mask: {a: :+, b: [1, :+, 3]},
|
230
|
+
result: []
|
231
|
+
|
232
|
+
context 'when comparing arrays' do
|
233
|
+
example 'reports the tree being shorter',
|
234
|
+
tree: {a: [1]},
|
235
|
+
mask: {a: [1, 2]},
|
236
|
+
result: [{path: 'a/1', tree: :__missing__, mask: [2]}]
|
237
|
+
|
238
|
+
example 'reports the mask being shorter',
|
239
|
+
tree: {a: [1, 2]},
|
240
|
+
mask: {a: [1]},
|
241
|
+
result: [{path: 'a/1', tree: [2], mask: :__absent__}]
|
242
|
+
|
243
|
+
example 'can allow arbitrary tails with :*',
|
244
|
+
tree: {a: [1, 2, 3], b: [1], c: [2]},
|
245
|
+
mask: {a: [1, :*], b: [1, :*], c: [1, :*]},
|
246
|
+
result: [{path: 'c/0', tree: 2, mask: 1}]
|
247
|
+
end
|
248
|
+
|
249
|
+
context 'when comparing sets' do
|
250
|
+
|
251
|
+
def self.get_name
|
252
|
+
->(x){ x[:name] }
|
253
|
+
end
|
254
|
+
|
255
|
+
example 'allows out-of-order arrays',
|
256
|
+
tree: {set: [{id: 2, name: 'b'}, {id: 1, name: 'a'}]},
|
257
|
+
mask: {set: set_mask([{id: 1, name: 'a'}, {id: 2, name: 'b'}], get_name)},
|
258
|
+
result: []
|
259
|
+
|
260
|
+
example 'reports missing set attribute in the tree',
|
261
|
+
tree: {},
|
262
|
+
mask: {set: set_mask([{id: 1, name: 'a'}], get_name)},
|
263
|
+
result: [{path: 'set', tree: :__missing__, mask: [{id: 1, name: 'a'}]}]
|
264
|
+
|
265
|
+
example 'reports missing items in the tree',
|
266
|
+
tree: {set: []},
|
267
|
+
mask: {set: set_mask([{id: 1, name: 'a'}], get_name)},
|
268
|
+
result: [{path: 'set/a', tree: :__missing__, mask: {id: 1, name: 'a'}}]
|
269
|
+
|
270
|
+
example 'reports extra items in the tree',
|
271
|
+
tree: {set: [{id: 1, name: 'a'}]},
|
272
|
+
mask: {set: set_mask([], get_name)},
|
273
|
+
result: [{path: 'set/a', tree: {id: 1, name: 'a'}, mask: :__absent__}]
|
274
|
+
|
275
|
+
example 'reports duplicate keys in the tree',
|
276
|
+
tree: {set: [{id: 1, name: 'a'}, {id: 2, name: 'a'}]},
|
277
|
+
mask: {set: set_mask([:*], get_name)},
|
278
|
+
result: [{path: 'set', tree: [:__duplicate_keys__, ['a']], mask: nil}]
|
279
|
+
|
280
|
+
example 'reports duplicate keys in the mask',
|
281
|
+
tree: {set: [{id: 1, name: 'a'}]},
|
282
|
+
mask: {set: set_mask([{id: 1, name: 'a'}, {id: 2, name: 'a'}], get_name)},
|
283
|
+
result: [{path: 'set', tree: nil, mask: [:__duplicate_keys__, ['a']]}]
|
284
|
+
|
285
|
+
example 'can test for only a subset',
|
286
|
+
tree: {set: [{id: 1, name: 'a'}, {id: 2, name: 'b'}]},
|
287
|
+
mask: {set: set_mask([{id: 2, name: 'b'}, :*], get_name)},
|
288
|
+
result: []
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
|
293
|
+
context 'reports mismatched types' do
|
294
|
+
example 'hash for primitive',
|
295
|
+
tree: {a: {b: 1}},
|
296
|
+
mask: {a: 1},
|
297
|
+
result: [{path: 'a', tree: {b: 1}, mask: 1}]
|
298
|
+
|
299
|
+
example 'primitive for hash',
|
300
|
+
tree: {a: 1},
|
301
|
+
mask: {a: {b: 1}},
|
302
|
+
result: [{path: 'a', tree: 1, mask: {b: 1}}]
|
303
|
+
|
304
|
+
example 'array for primitive',
|
305
|
+
tree: {a: [1, 2]},
|
306
|
+
mask: {a: 1},
|
307
|
+
result: [{path: 'a', tree: [1, 2], mask: 1}]
|
308
|
+
|
309
|
+
example 'primitive for array',
|
310
|
+
tree: {a: 1},
|
311
|
+
mask: {a: [1, 2]},
|
312
|
+
result: [{path: 'a', tree: 1, mask: [1, 2]}]
|
313
|
+
|
314
|
+
example 'primitive for set',
|
315
|
+
tree: {set: 1},
|
316
|
+
mask: {set: set_mask([{x: 1}], ->(item) { item[:x] })},
|
317
|
+
result: [{path: 'set', tree: 1, mask: [{x: 1}]}]
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|