abstractivator 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module Abstractivator
2
+ VERSION = '0.0.15'
3
+ 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