closure_tree 6.5.0 → 7.4.0
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 +5 -5
- data/.github/workflows/ci.yml +98 -0
- data/.gitignore +2 -0
- data/.rspec +1 -1
- data/Appraisals +90 -7
- data/CHANGELOG.md +100 -42
- data/Gemfile +3 -11
- data/README.md +68 -24
- data/Rakefile +16 -10
- data/_config.yml +1 -0
- data/bin/appraisal +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/closure_tree.gemspec +16 -9
- data/lib/closure_tree/finders.rb +32 -9
- data/lib/closure_tree/has_closure_tree.rb +4 -0
- data/lib/closure_tree/has_closure_tree_root.rb +5 -7
- data/lib/closure_tree/hash_tree_support.rb +4 -4
- data/lib/closure_tree/hierarchy_maintenance.rb +28 -8
- data/lib/closure_tree/model.rb +42 -16
- data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
- data/lib/closure_tree/numeric_order_support.rb +7 -3
- data/lib/closure_tree/support.rb +18 -12
- data/lib/closure_tree/support_attributes.rb +10 -1
- data/lib/closure_tree/support_flags.rb +1 -4
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +8 -0
- data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
- metadata +78 -79
- data/.travis.yml +0 -29
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
- data/gemfiles/activerecord_edge.gemfile +0 -20
- data/img/example.png +0 -0
- data/img/preorder.png +0 -0
- data/spec/cache_invalidation_spec.rb +0 -39
- data/spec/cuisine_type_spec.rb +0 -38
- data/spec/db/database.yml +0 -21
- data/spec/db/models.rb +0 -128
- data/spec/db/schema.rb +0 -166
- data/spec/fixtures/tags.yml +0 -98
- data/spec/generators/migration_generator_spec.rb +0 -48
- data/spec/has_closure_tree_root_spec.rb +0 -154
- data/spec/hierarchy_maintenance_spec.rb +0 -16
- data/spec/label_spec.rb +0 -554
- data/spec/matcher_spec.rb +0 -34
- data/spec/metal_spec.rb +0 -55
- data/spec/model_spec.rb +0 -9
- data/spec/namespace_type_spec.rb +0 -13
- data/spec/parallel_spec.rb +0 -159
- data/spec/spec_helper.rb +0 -24
- data/spec/support/database.rb +0 -52
- data/spec/support/database_cleaner.rb +0 -14
- data/spec/support/exceed_query_limit.rb +0 -18
- data/spec/support/hash_monkey_patch.rb +0 -13
- data/spec/support/query_counter.rb +0 -18
- data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
- data/spec/support_spec.rb +0 -14
- data/spec/tag_examples.rb +0 -665
- data/spec/tag_spec.rb +0 -6
- data/spec/user_spec.rb +0 -174
- data/spec/uuid_tag_spec.rb +0 -6
data/spec/tag_examples.rb
DELETED
@@ -1,665 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
shared_examples_for Tag do
|
4
|
-
|
5
|
-
let (:tag_class) { described_class }
|
6
|
-
let (:tag_hierarchy_class) { described_class.hierarchy_class }
|
7
|
-
|
8
|
-
context 'class setup' do
|
9
|
-
|
10
|
-
it 'has correct accessible_attributes' do
|
11
|
-
if tag_class._ct.use_attr_accessible?
|
12
|
-
expect(tag_class.accessible_attributes.to_a).to match_array(%w(parent name title))
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
it 'should build hierarchy classname correctly' do
|
17
|
-
expect(tag_class.hierarchy_class).to eq(tag_hierarchy_class)
|
18
|
-
expect(tag_class._ct.hierarchy_class_name).to eq(tag_hierarchy_class.to_s)
|
19
|
-
expect(tag_class._ct.short_hierarchy_class_name).to eq(tag_hierarchy_class.to_s)
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'should have a correct parent column name' do
|
23
|
-
expected_parent_column_name = tag_class == UUIDTag ? 'parent_uuid' : 'parent_id'
|
24
|
-
expect(tag_class._ct.parent_column_name).to eq(expected_parent_column_name)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
describe 'from empty db' do
|
29
|
-
|
30
|
-
context 'with no tags' do
|
31
|
-
it 'should return no entities' do
|
32
|
-
expect(tag_class.roots).to be_empty
|
33
|
-
expect(tag_class.leaves).to be_empty
|
34
|
-
end
|
35
|
-
|
36
|
-
it '#find_or_create_by_path with strings' do
|
37
|
-
a = tag_class.create!(name: 'a')
|
38
|
-
expect(a.find_or_create_by_path(%w{b c}).ancestry_path).to eq(%w{a b c})
|
39
|
-
end
|
40
|
-
|
41
|
-
it '#find_or_create_by_path with hashes' do
|
42
|
-
a = tag_class.create!(name: 'a', title: 'A')
|
43
|
-
subject = a.find_or_create_by_path([
|
44
|
-
{name: 'b', title: 'B'},
|
45
|
-
{name: 'c', title: 'C'}
|
46
|
-
])
|
47
|
-
expect(subject.ancestry_path).to eq(%w{a b c})
|
48
|
-
expect(subject.self_and_ancestors.map(&:title)).to eq(%w{C B A})
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context 'with 1 tag' do
|
53
|
-
before do
|
54
|
-
@tag = tag_class.create!(name: 'tag')
|
55
|
-
end
|
56
|
-
|
57
|
-
it 'should be a leaf' do
|
58
|
-
expect(@tag.leaf?).to be_truthy
|
59
|
-
end
|
60
|
-
|
61
|
-
it 'should be a root' do
|
62
|
-
expect(@tag.root?).to be_truthy
|
63
|
-
end
|
64
|
-
|
65
|
-
it 'has no parent' do
|
66
|
-
expect(@tag.parent).to be_nil
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'should return the only entity as a root and leaf' do
|
70
|
-
expect(tag_class.all).to eq([@tag])
|
71
|
-
expect(tag_class.roots).to eq([@tag])
|
72
|
-
expect(tag_class.leaves).to eq([@tag])
|
73
|
-
end
|
74
|
-
|
75
|
-
context 'with child' do
|
76
|
-
before do
|
77
|
-
@child = tag_class.create!(name: 'tag 2')
|
78
|
-
end
|
79
|
-
|
80
|
-
def assert_roots_and_leaves
|
81
|
-
expect(@tag.root?).to be_truthy
|
82
|
-
expect(@tag.leaf?).to be_falsey
|
83
|
-
|
84
|
-
expect(@child.root?).to be_falsey
|
85
|
-
expect(@child.leaf?).to be_truthy
|
86
|
-
end
|
87
|
-
|
88
|
-
def assert_parent_and_children
|
89
|
-
expect(@child.reload.parent).to eq(@tag)
|
90
|
-
expect(@tag.reload.children.to_a).to eq([@child])
|
91
|
-
end
|
92
|
-
|
93
|
-
it 'adds children through add_child' do
|
94
|
-
@tag.add_child @child
|
95
|
-
assert_roots_and_leaves
|
96
|
-
assert_parent_and_children
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'adds children through collection' do
|
100
|
-
@tag.children << @child
|
101
|
-
assert_roots_and_leaves
|
102
|
-
assert_parent_and_children
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
context 'with 2 tags' do
|
108
|
-
before :each do
|
109
|
-
@root = tag_class.create!(name: 'root')
|
110
|
-
@leaf = @root.add_child(tag_class.create!(name: 'leaf'))
|
111
|
-
end
|
112
|
-
it 'should return a simple root and leaf' do
|
113
|
-
expect(tag_class.roots).to eq([@root])
|
114
|
-
expect(tag_class.leaves).to eq([@leaf])
|
115
|
-
end
|
116
|
-
it 'should return child_ids for root' do
|
117
|
-
expect(@root.child_ids).to eq([@leaf.id])
|
118
|
-
end
|
119
|
-
|
120
|
-
it 'should return an empty array for leaves' do
|
121
|
-
expect(@leaf.child_ids).to be_empty
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
context '3 tag collection.create db' do
|
126
|
-
before :each do
|
127
|
-
@root = tag_class.create! name: 'root'
|
128
|
-
@mid = @root.children.create! name: 'mid'
|
129
|
-
@leaf = @mid.children.create! name: 'leaf'
|
130
|
-
DestroyedTag.delete_all
|
131
|
-
end
|
132
|
-
|
133
|
-
it 'should create all tags' do
|
134
|
-
expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
|
135
|
-
end
|
136
|
-
|
137
|
-
it 'should return a root and leaf without middle tag' do
|
138
|
-
expect(tag_class.roots).to eq([@root])
|
139
|
-
expect(tag_class.leaves).to eq([@leaf])
|
140
|
-
end
|
141
|
-
|
142
|
-
it 'should delete leaves' do
|
143
|
-
tag_class.leaves.destroy_all
|
144
|
-
expect(tag_class.roots).to eq([@root]) # untouched
|
145
|
-
expect(tag_class.leaves).to eq([@mid])
|
146
|
-
end
|
147
|
-
|
148
|
-
it 'should delete everything if you delete the roots' do
|
149
|
-
tag_class.roots.destroy_all
|
150
|
-
expect(tag_class.all).to be_empty
|
151
|
-
expect(tag_class.roots).to be_empty
|
152
|
-
expect(tag_class.leaves).to be_empty
|
153
|
-
expect(DestroyedTag.all.map { |t| t.name }).to match_array(%w{root mid leaf})
|
154
|
-
end
|
155
|
-
|
156
|
-
it 'fix self_and_ancestors properly on reparenting' do
|
157
|
-
t = tag_class.create! name: 'moar leaf'
|
158
|
-
expect(t.self_and_ancestors.to_a).to eq([t])
|
159
|
-
@mid.children << t
|
160
|
-
expect(t.self_and_ancestors.to_a).to eq([t, @mid, @root])
|
161
|
-
end
|
162
|
-
|
163
|
-
it 'prevents ancestor loops' do
|
164
|
-
@leaf.add_child @root
|
165
|
-
expect(@root).not_to be_valid
|
166
|
-
expect(@root.reload.descendants).to include(@leaf)
|
167
|
-
end
|
168
|
-
|
169
|
-
it 'moves non-leaves' do
|
170
|
-
new_root = tag_class.create! name: 'new_root'
|
171
|
-
new_root.children << @mid
|
172
|
-
expect(@root.reload.descendants).to be_empty
|
173
|
-
expect(new_root.descendants).to eq([@mid, @leaf])
|
174
|
-
expect(@leaf.reload.ancestry_path).to eq(%w{new_root mid leaf})
|
175
|
-
end
|
176
|
-
|
177
|
-
it 'moves leaves' do
|
178
|
-
new_root = tag_class.create! name: 'new_root'
|
179
|
-
new_root.children << @leaf
|
180
|
-
expect(new_root.descendants).to eq([@leaf])
|
181
|
-
expect(@root.reload.descendants).to eq([@mid])
|
182
|
-
expect(@leaf.reload.ancestry_path).to eq(%w{new_root leaf})
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
context '3 tag explicit_create db' do
|
187
|
-
before :each do
|
188
|
-
@root = tag_class.create!(name: 'root')
|
189
|
-
@mid = @root.add_child(tag_class.create!(name: 'mid'))
|
190
|
-
@leaf = @mid.add_child(tag_class.create!(name: 'leaf'))
|
191
|
-
end
|
192
|
-
|
193
|
-
it 'should create all tags' do
|
194
|
-
expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
|
195
|
-
end
|
196
|
-
|
197
|
-
it 'should return a root and leaf without middle tag' do
|
198
|
-
expect(tag_class.roots).to eq([@root])
|
199
|
-
expect(tag_class.leaves).to eq([@leaf])
|
200
|
-
end
|
201
|
-
|
202
|
-
it 'should prevent parental loops from torso' do
|
203
|
-
@mid.children << @root
|
204
|
-
expect(@root.valid?).to be_falsey
|
205
|
-
expect(@mid.reload.children).to eq([@leaf])
|
206
|
-
end
|
207
|
-
|
208
|
-
it 'should prevent parental loops from toes' do
|
209
|
-
@leaf.children << @root
|
210
|
-
expect(@root.valid?).to be_falsey
|
211
|
-
expect(@leaf.reload.children).to be_empty
|
212
|
-
end
|
213
|
-
|
214
|
-
it 'should support re-parenting' do
|
215
|
-
@root.children << @leaf
|
216
|
-
expect(tag_class.leaves).to eq([@leaf, @mid])
|
217
|
-
end
|
218
|
-
|
219
|
-
it 'cleans up hierarchy references for leaves' do
|
220
|
-
@leaf.destroy
|
221
|
-
expect(tag_hierarchy_class.where(ancestor_id: @leaf.id)).to be_empty
|
222
|
-
expect(tag_hierarchy_class.where(descendant_id: @leaf.id)).to be_empty
|
223
|
-
end
|
224
|
-
|
225
|
-
it 'cleans up hierarchy references' do
|
226
|
-
@mid.destroy
|
227
|
-
expect(tag_hierarchy_class.where(ancestor_id: @mid.id)).to be_empty
|
228
|
-
expect(tag_hierarchy_class.where(descendant_id: @mid.id)).to be_empty
|
229
|
-
expect(@root.reload).to be_root
|
230
|
-
root_hiers = @root.ancestor_hierarchies.to_a
|
231
|
-
expect(root_hiers.size).to eq(1)
|
232
|
-
expect(tag_hierarchy_class.where(ancestor_id: @root.id)).to eq(root_hiers)
|
233
|
-
expect(tag_hierarchy_class.where(descendant_id: @root.id)).to eq(root_hiers)
|
234
|
-
end
|
235
|
-
|
236
|
-
it 'should have different hash codes for each hierarchy model' do
|
237
|
-
hashes = tag_hierarchy_class.all.map(&:hash)
|
238
|
-
expect(hashes).to match_array(hashes.uniq)
|
239
|
-
end
|
240
|
-
|
241
|
-
it 'should return the same hash code for equal hierarchy models' do
|
242
|
-
expect(tag_hierarchy_class.first.hash).to eq(tag_hierarchy_class.first.hash)
|
243
|
-
end
|
244
|
-
end
|
245
|
-
|
246
|
-
it 'performs as the readme says it does' do
|
247
|
-
grandparent = tag_class.create(name: 'Grandparent')
|
248
|
-
parent = grandparent.children.create(name: 'Parent')
|
249
|
-
child1 = tag_class.create(name: 'First Child', parent: parent)
|
250
|
-
child2 = tag_class.new(name: 'Second Child')
|
251
|
-
parent.children << child2
|
252
|
-
child3 = tag_class.new(name: 'Third Child')
|
253
|
-
parent.add_child child3
|
254
|
-
expect(grandparent.self_and_descendants.collect(&:name)).to eq(
|
255
|
-
['Grandparent', 'Parent', 'First Child', 'Second Child', 'Third Child']
|
256
|
-
)
|
257
|
-
expect(child1.ancestry_path).to eq(
|
258
|
-
['Grandparent', 'Parent', 'First Child']
|
259
|
-
)
|
260
|
-
expect(child3.ancestry_path).to eq(
|
261
|
-
['Grandparent', 'Parent', 'Third Child']
|
262
|
-
)
|
263
|
-
d = tag_class.find_or_create_by_path %w(a b c d)
|
264
|
-
h = tag_class.find_or_create_by_path %w(e f g h)
|
265
|
-
e = h.root
|
266
|
-
d.add_child(e) # "d.children << e" would work too, of course
|
267
|
-
expect(h.ancestry_path).to eq(%w(a b c d e f g h))
|
268
|
-
end
|
269
|
-
|
270
|
-
it 'roots sort alphabetically' do
|
271
|
-
expected = ('a'..'z').to_a
|
272
|
-
expected.shuffle.each { |ea| tag_class.create!(name: ea) }
|
273
|
-
expect(tag_class.roots.collect { |ea| ea.name }).to eq(expected)
|
274
|
-
end
|
275
|
-
|
276
|
-
context 'with simple tree' do
|
277
|
-
before :each do
|
278
|
-
tag_class.find_or_create_by_path %w(a1 b1 c1a)
|
279
|
-
tag_class.find_or_create_by_path %w(a1 b1 c1b)
|
280
|
-
tag_class.find_or_create_by_path %w(a1 b1 c1c)
|
281
|
-
tag_class.find_or_create_by_path %w(a1 b1b)
|
282
|
-
tag_class.find_or_create_by_path %w(a2 b2)
|
283
|
-
tag_class.find_or_create_by_path %w(a3)
|
284
|
-
|
285
|
-
@a1, @a2, @a3, @b1, @b1b, @b2, @c1a, @c1b, @c1c =
|
286
|
-
tag_class.all.sort_by(&:name)
|
287
|
-
@expected_roots = [@a1, @a2, @a3]
|
288
|
-
@expected_leaves = [@c1a, @c1b, @c1c, @b1b, @b2, @a3]
|
289
|
-
@expected_siblings = [[@a1, @a2, @a3], [@b1, @b1b], [@c1a, @c1b, @c1c]]
|
290
|
-
@expected_only_children = tag_class.all - @expected_siblings.flatten
|
291
|
-
end
|
292
|
-
|
293
|
-
it 'should find global roots' do
|
294
|
-
expect(tag_class.roots.to_a).to match_array(@expected_roots)
|
295
|
-
end
|
296
|
-
it 'should return root? for roots' do
|
297
|
-
@expected_roots.each { |ea| expect(ea).to be_root }
|
298
|
-
end
|
299
|
-
it 'should not return root? for non-roots' do
|
300
|
-
[@b1, @b2, @c1a, @c1b].each { |ea| expect(ea).not_to be_root }
|
301
|
-
end
|
302
|
-
it 'should return the correct root' do
|
303
|
-
{@a1 => @a1, @a2 => @a2, @a3 => @a3,
|
304
|
-
@b1 => @a1, @b2 => @a2, @c1a => @a1, @c1b => @a1}.each do |node, root|
|
305
|
-
expect(node.root).to eq(root)
|
306
|
-
end
|
307
|
-
end
|
308
|
-
it 'should assemble global leaves' do
|
309
|
-
expect(tag_class.leaves.to_a).to match_array(@expected_leaves)
|
310
|
-
end
|
311
|
-
it 'assembles siblings properly' do
|
312
|
-
@expected_siblings.each do |siblings|
|
313
|
-
siblings.each do |ea|
|
314
|
-
expect(ea.self_and_siblings.to_a).to match_array(siblings)
|
315
|
-
expect(ea.siblings.to_a).to match_array(siblings - [ea])
|
316
|
-
end
|
317
|
-
end
|
318
|
-
@expected_only_children.each do |ea|
|
319
|
-
expect(ea.siblings).to eq([])
|
320
|
-
end
|
321
|
-
end
|
322
|
-
it 'assembles before_siblings' do
|
323
|
-
@expected_siblings.each do |siblings|
|
324
|
-
(siblings.size - 1).times do |i|
|
325
|
-
target = siblings[i]
|
326
|
-
expected_before = siblings.first(i)
|
327
|
-
expect(target.siblings_before.to_a).to eq(expected_before)
|
328
|
-
end
|
329
|
-
end
|
330
|
-
end
|
331
|
-
it 'assembles after_siblings' do
|
332
|
-
@expected_siblings.each do |siblings|
|
333
|
-
(siblings.size - 1).times do |i|
|
334
|
-
target = siblings[i]
|
335
|
-
expected_after = siblings.last(siblings.size - 1 - i)
|
336
|
-
expect(target.siblings_after.to_a).to eq(expected_after)
|
337
|
-
end
|
338
|
-
end
|
339
|
-
end
|
340
|
-
it 'should assemble instance leaves' do
|
341
|
-
{@a1 => [@b1b, @c1a, @c1b, @c1c], @b1 => [@c1a, @c1b, @c1c], @a2 => [@b2]}.each do |node, leaves|
|
342
|
-
expect(node.leaves.to_a).to eq(leaves)
|
343
|
-
end
|
344
|
-
@expected_leaves.each { |ea| expect(ea.leaves.to_a).to eq([ea]) }
|
345
|
-
end
|
346
|
-
it 'should return leaf? for leaves' do
|
347
|
-
@expected_leaves.each { |ea| expect(ea).to be_leaf }
|
348
|
-
end
|
349
|
-
|
350
|
-
it 'can move roots' do
|
351
|
-
@c1a.children << @a2
|
352
|
-
@b2.reload.children << @a3
|
353
|
-
expect(@a3.reload.ancestry_path).to eq(%w(a1 b1 c1a a2 b2 a3))
|
354
|
-
end
|
355
|
-
|
356
|
-
it 'cascade-deletes from roots' do
|
357
|
-
victim_names = @a1.self_and_descendants.map(&:name)
|
358
|
-
survivor_names = tag_class.all.map(&:name) - victim_names
|
359
|
-
@a1.destroy
|
360
|
-
expect(tag_class.all.map(&:name)).to eq(survivor_names)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
context 'with_ancestor' do
|
365
|
-
it 'works with no rows' do
|
366
|
-
expect(tag_class.with_ancestor.to_a).to be_empty
|
367
|
-
end
|
368
|
-
it 'finds only children' do
|
369
|
-
c = tag_class.find_or_create_by_path %w(A B C)
|
370
|
-
a, b = c.parent.parent, c.parent
|
371
|
-
spurious_tags = tag_class.find_or_create_by_path %w(D E)
|
372
|
-
expect(tag_class.with_ancestor(a).to_a).to eq([b, c])
|
373
|
-
end
|
374
|
-
it 'limits subsequent where clauses' do
|
375
|
-
a1c = tag_class.find_or_create_by_path %w(A1 B C)
|
376
|
-
a2c = tag_class.find_or_create_by_path %w(A2 B C)
|
377
|
-
# different paths!
|
378
|
-
expect(a1c).not_to eq(a2c)
|
379
|
-
expect(tag_class.where(:name => 'C').to_a).to match_array([a1c, a2c])
|
380
|
-
expect(tag_class.with_ancestor(a1c.parent.parent).where(:name => 'C').to_a).to eq([a1c])
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
context 'paths' do
|
385
|
-
context 'with grandchild' do
|
386
|
-
before do
|
387
|
-
@child = tag_class.find_or_create_by_path([
|
388
|
-
{name: 'grandparent', title: 'Nonnie'},
|
389
|
-
{name: 'parent', title: 'Mom'},
|
390
|
-
{name: 'child', title: 'Kid'}])
|
391
|
-
@parent = @child.parent
|
392
|
-
@grandparent = @parent.parent
|
393
|
-
end
|
394
|
-
|
395
|
-
it 'should build ancestry path' do
|
396
|
-
expect(@child.ancestry_path).to eq(%w{grandparent parent child})
|
397
|
-
expect(@child.ancestry_path(:name)).to eq(%w{grandparent parent child})
|
398
|
-
expect(@child.ancestry_path(:title)).to eq(%w{Nonnie Mom Kid})
|
399
|
-
end
|
400
|
-
|
401
|
-
it 'assembles ancestors' do
|
402
|
-
expect(@child.ancestors).to eq([@parent, @grandparent])
|
403
|
-
expect(@child.self_and_ancestors).to eq([@child, @parent, @grandparent])
|
404
|
-
end
|
405
|
-
|
406
|
-
it 'should find by path' do
|
407
|
-
# class method:
|
408
|
-
expect(tag_class.find_by_path(%w{grandparent parent child})).to eq(@child)
|
409
|
-
# instance method:
|
410
|
-
expect(@parent.find_by_path(%w{child})).to eq(@child)
|
411
|
-
expect(@grandparent.find_by_path(%w{parent child})).to eq(@child)
|
412
|
-
expect(@parent.find_by_path(%w{child larvae})).to be_nil
|
413
|
-
end
|
414
|
-
|
415
|
-
it 'should respect attribute hashes with both selection and creation' do
|
416
|
-
expected_title = 'something else'
|
417
|
-
attrs = {title: expected_title}
|
418
|
-
existing_title = @grandparent.title
|
419
|
-
new_grandparent = tag_class.find_or_create_by_path(%w{grandparent}, attrs)
|
420
|
-
expect(new_grandparent).not_to eq(@grandparent)
|
421
|
-
expect(new_grandparent.title).to eq(expected_title)
|
422
|
-
expect(@grandparent.reload.title).to eq(existing_title)
|
423
|
-
end
|
424
|
-
|
425
|
-
it 'should create a hierarchy with a given attribute' do
|
426
|
-
expected_title = 'unicorn rainbows'
|
427
|
-
attrs = {title: expected_title}
|
428
|
-
child = tag_class.find_or_create_by_path(%w{grandparent parent child}, attrs)
|
429
|
-
expect(child).not_to eq(@child)
|
430
|
-
[child, child.parent, child.parent.parent].each do |ea|
|
431
|
-
expect(ea.title).to eq(expected_title)
|
432
|
-
end
|
433
|
-
end
|
434
|
-
end
|
435
|
-
|
436
|
-
it 'finds correctly rooted paths' do
|
437
|
-
decoy = tag_class.find_or_create_by_path %w(a b c d)
|
438
|
-
b_d = tag_class.find_or_create_by_path %w(b c d)
|
439
|
-
expect(tag_class.find_by_path(%w(b c d))).to eq(b_d)
|
440
|
-
expect(tag_class.find_by_path(%w(c d))).to be_nil
|
441
|
-
end
|
442
|
-
|
443
|
-
it 'find_by_path for 1 node' do
|
444
|
-
b = tag_class.find_or_create_by_path %w(a b)
|
445
|
-
b2 = b.root.find_by_path(%w(b))
|
446
|
-
expect(b2).to eq(b)
|
447
|
-
end
|
448
|
-
|
449
|
-
it 'find_by_path for 2 nodes' do
|
450
|
-
path = %w(a b c)
|
451
|
-
c = tag_class.find_or_create_by_path path
|
452
|
-
permutations = path.permutation.to_a
|
453
|
-
correct = %w(b c)
|
454
|
-
expect(c.root.find_by_path(correct)).to eq(c)
|
455
|
-
(permutations - correct).each do |bad_path|
|
456
|
-
expect(c.root.find_by_path(bad_path)).to be_nil
|
457
|
-
end
|
458
|
-
end
|
459
|
-
|
460
|
-
it 'find_by_path for 3 nodes' do
|
461
|
-
d = tag_class.find_or_create_by_path %w(a b c d)
|
462
|
-
expect(d.root.find_by_path(%w(b c d))).to eq(d)
|
463
|
-
expect(tag_class.find_by_path(%w(a b c d))).to eq(d)
|
464
|
-
expect(tag_class.find_by_path(%w(d))).to be_nil
|
465
|
-
end
|
466
|
-
|
467
|
-
it 'should return nil for missing nodes' do
|
468
|
-
expect(tag_class.find_by_path(%w{missing})).to be_nil
|
469
|
-
expect(tag_class.find_by_path(%w{grandparent missing})).to be_nil
|
470
|
-
expect(tag_class.find_by_path(%w{grandparent parent missing})).to be_nil
|
471
|
-
expect(tag_class.find_by_path(%w{grandparent parent missing child})).to be_nil
|
472
|
-
end
|
473
|
-
|
474
|
-
describe '.find_or_create_by_path' do
|
475
|
-
it 'uses existing records' do
|
476
|
-
grandparent = tag_class.find_or_create_by_path(%w{grandparent})
|
477
|
-
expect(grandparent).to eq(grandparent)
|
478
|
-
child = tag_class.find_or_create_by_path(%w{grandparent parent child})
|
479
|
-
expect(child).to eq(child)
|
480
|
-
end
|
481
|
-
|
482
|
-
it 'creates 2-deep trees with strings' do
|
483
|
-
subject = tag_class.find_or_create_by_path(%w{events anniversary})
|
484
|
-
expect(subject.ancestry_path).to eq(%w{events anniversary})
|
485
|
-
end
|
486
|
-
|
487
|
-
it 'creates 2-deep trees with hashes' do
|
488
|
-
subject = tag_class.find_or_create_by_path([
|
489
|
-
{name: 'test1', title: 'TEST1'},
|
490
|
-
{name: 'test2', title: 'TEST2'}
|
491
|
-
])
|
492
|
-
expect(subject.ancestry_path).to eq(%w{test1 test2})
|
493
|
-
# `self_and_ancestors` and `ancestors` is ordered parent-first. (!!)
|
494
|
-
expect(subject.self_and_ancestors.map(&:title)).to eq(%w{TEST2 TEST1})
|
495
|
-
end
|
496
|
-
|
497
|
-
end
|
498
|
-
end
|
499
|
-
|
500
|
-
context 'hash_tree' do
|
501
|
-
before :each do
|
502
|
-
@d1 = tag_class.find_or_create_by_path %w(a b c1 d1)
|
503
|
-
@c1 = @d1.parent
|
504
|
-
@b = @c1.parent
|
505
|
-
@a = @b.parent
|
506
|
-
@a2 = tag_class.create(name: 'a2')
|
507
|
-
@b2 = tag_class.find_or_create_by_path %w(a b2)
|
508
|
-
@c3 = tag_class.find_or_create_by_path %w(a3 b3 c3)
|
509
|
-
@b3 = @c3.parent
|
510
|
-
@a3 = @b3.parent
|
511
|
-
@tree2 = {
|
512
|
-
@a => {@b => {}, @b2 => {}}, @a2 => {}, @a3 => {@b3 => {}}
|
513
|
-
}
|
514
|
-
|
515
|
-
@one_tree = {
|
516
|
-
@a => {},
|
517
|
-
@a2 => {},
|
518
|
-
@a3 => {}
|
519
|
-
}
|
520
|
-
@two_tree = {
|
521
|
-
@a => {
|
522
|
-
@b => {},
|
523
|
-
@b2 => {}
|
524
|
-
},
|
525
|
-
@a2 => {},
|
526
|
-
@a3 => {
|
527
|
-
@b3 => {}
|
528
|
-
}
|
529
|
-
}
|
530
|
-
@three_tree = {
|
531
|
-
@a => {
|
532
|
-
@b => {
|
533
|
-
@c1 => {},
|
534
|
-
},
|
535
|
-
@b2 => {}
|
536
|
-
},
|
537
|
-
@a2 => {},
|
538
|
-
@a3 => {
|
539
|
-
@b3 => {
|
540
|
-
@c3 => {}
|
541
|
-
}
|
542
|
-
}
|
543
|
-
}
|
544
|
-
@full_tree = {
|
545
|
-
@a => {
|
546
|
-
@b => {
|
547
|
-
@c1 => {
|
548
|
-
@d1 => {}
|
549
|
-
},
|
550
|
-
},
|
551
|
-
@b2 => {}
|
552
|
-
},
|
553
|
-
@a2 => {},
|
554
|
-
@a3 => {
|
555
|
-
@b3 => {
|
556
|
-
@c3 => {}
|
557
|
-
}
|
558
|
-
}
|
559
|
-
}
|
560
|
-
#File.open("example.dot", "w") { |f| f.write(tag_class.root.to_dot_digraph) }
|
561
|
-
end
|
562
|
-
|
563
|
-
context '#hash_tree' do
|
564
|
-
it 'returns {} for depth 0' do
|
565
|
-
expect(tag_class.hash_tree(limit_depth: 0)).to eq({})
|
566
|
-
end
|
567
|
-
it 'limit_depth 1' do
|
568
|
-
expect(tag_class.hash_tree(limit_depth: 1)).to eq(@one_tree)
|
569
|
-
end
|
570
|
-
it 'limit_depth 2' do
|
571
|
-
expect(tag_class.hash_tree(limit_depth: 2)).to eq(@two_tree)
|
572
|
-
end
|
573
|
-
it 'limit_depth 3' do
|
574
|
-
expect(tag_class.hash_tree(limit_depth: 3)).to eq(@three_tree)
|
575
|
-
end
|
576
|
-
it 'limit_depth 4' do
|
577
|
-
expect(tag_class.hash_tree(limit_depth: 4)).to eq(@full_tree)
|
578
|
-
end
|
579
|
-
it 'no limit' do
|
580
|
-
expect(tag_class.hash_tree).to eq(@full_tree)
|
581
|
-
end
|
582
|
-
end
|
583
|
-
|
584
|
-
context '.hash_tree' do
|
585
|
-
it 'returns {} for depth 0' do
|
586
|
-
expect(@b.hash_tree(limit_depth: 0)).to eq({})
|
587
|
-
end
|
588
|
-
it 'limit_depth 1' do
|
589
|
-
expect(@b.hash_tree(limit_depth: 1)).to eq(@two_tree[@a].slice(@b))
|
590
|
-
end
|
591
|
-
it 'limit_depth 2' do
|
592
|
-
expect(@b.hash_tree(limit_depth: 2)).to eq(@three_tree[@a].slice(@b))
|
593
|
-
end
|
594
|
-
it 'limit_depth 3' do
|
595
|
-
expect(@b.hash_tree(limit_depth: 3)).to eq(@full_tree[@a].slice(@b))
|
596
|
-
end
|
597
|
-
it 'no limit from subsubroot' do
|
598
|
-
expect(@c1.hash_tree).to eq(@full_tree[@a][@b].slice(@c1))
|
599
|
-
end
|
600
|
-
it 'no limit from subroot' do
|
601
|
-
expect(@b.hash_tree).to eq(@full_tree[@a].slice(@b))
|
602
|
-
end
|
603
|
-
it 'no limit from root' do
|
604
|
-
expect(@a.hash_tree.merge(@a2.hash_tree)).to eq(@full_tree.slice(@a, @a2))
|
605
|
-
end
|
606
|
-
end
|
607
|
-
|
608
|
-
context '.hash_tree from relations' do
|
609
|
-
it 'limit_depth 2 from chained activerecord association subroots' do
|
610
|
-
expect(@a.children.hash_tree(limit_depth: 2)).to eq(@three_tree[@a])
|
611
|
-
end
|
612
|
-
it 'no limit from chained activerecord association subroots' do
|
613
|
-
expect(@a.children.hash_tree).to eq(@full_tree[@a])
|
614
|
-
end
|
615
|
-
it 'limit_depth 3 from b.parent' do
|
616
|
-
expect(@b.parent.hash_tree(limit_depth: 3)).to eq(@three_tree.slice(@a))
|
617
|
-
end
|
618
|
-
it 'no limit_depth from b.parent' do
|
619
|
-
expect(@b.parent.hash_tree).to eq(@full_tree.slice(@a))
|
620
|
-
end
|
621
|
-
it 'no limit_depth from c.parent' do
|
622
|
-
expect(@c1.parent.hash_tree).to eq(@full_tree[@a].slice(@b))
|
623
|
-
end
|
624
|
-
end
|
625
|
-
end
|
626
|
-
|
627
|
-
it 'finds_by_path for very deep trees' do
|
628
|
-
expect(tag_class._ct).to receive(:max_join_tables).at_least(1).and_return(3)
|
629
|
-
path = (1..20).to_a.map { |ea| ea.to_s }
|
630
|
-
subject = tag_class.find_or_create_by_path(path)
|
631
|
-
expect(subject.ancestry_path).to eq(path)
|
632
|
-
expect(tag_class.find_by_path(path)).to eq(subject)
|
633
|
-
root = subject.root
|
634
|
-
expect(root.find_by_path(path[1..-1])).to eq(subject)
|
635
|
-
end
|
636
|
-
|
637
|
-
describe 'DOT rendering' do
|
638
|
-
it 'should render for an empty scope' do
|
639
|
-
expect(tag_class.to_dot_digraph(tag_class.where('0=1'))).to eq("digraph G {\n}\n")
|
640
|
-
end
|
641
|
-
it 'should render for an empty scope' do
|
642
|
-
tag_class.find_or_create_by_path(%w(a b1 c1))
|
643
|
-
tag_class.find_or_create_by_path(%w(a b2 c2))
|
644
|
-
tag_class.find_or_create_by_path(%w(a b2 c3))
|
645
|
-
a, b1, b2, c1, c2, c3 = %w(a b1 b2 c1 c2 c3).map { |ea| tag_class.where(name: ea).first.id }
|
646
|
-
dot = tag_class.roots.first.to_dot_digraph
|
647
|
-
expect(dot).to eq <<-DOT
|
648
|
-
digraph G {
|
649
|
-
"#{a}" [label="a"]
|
650
|
-
"#{a}" -> "#{b1}"
|
651
|
-
"#{b1}" [label="b1"]
|
652
|
-
"#{a}" -> "#{b2}"
|
653
|
-
"#{b2}" [label="b2"]
|
654
|
-
"#{b1}" -> "#{c1}"
|
655
|
-
"#{c1}" [label="c1"]
|
656
|
-
"#{b2}" -> "#{c2}"
|
657
|
-
"#{c2}" [label="c2"]
|
658
|
-
"#{b2}" -> "#{c3}"
|
659
|
-
"#{c3}" [label="c3"]
|
660
|
-
}
|
661
|
-
DOT
|
662
|
-
end
|
663
|
-
end
|
664
|
-
end
|
665
|
-
end
|