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.
@@ -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