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