hierarchable 0.2.1 → 0.3.1
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +41 -2
- data/lib/hierarchable/hierarchable.rb +110 -19
- data/lib/hierarchable/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77318268a18001072d590f7ad65524e5f608923cea6c6e757946ad164862518b
|
|
4
|
+
data.tar.gz: 7740d0be435b42dd701fc626eaa706b58c50688c0bb7639da9caf8b5864d3f3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '078378d734ddc00ccfcecf685fdd142e581560af7444a34985172c354eae1aeedb91ea98a2b08fd47a20728ce8d594c5d965a49409e7957236549b34352a852f'
|
|
7
|
+
data.tar.gz: 4b9192aec201abb47df5be7589a207c46131fb3544bb2b7cb439dd5c4d6a5df4fe09bc98e32aba14b72b2f3aa789295933226793cb282741a90f1dcd442f38ca
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
hierarchable (0.
|
|
4
|
+
hierarchable (0.3.1)
|
|
5
5
|
activerecord (> 4.2.0)
|
|
6
6
|
activesupport (> 4.2.0)
|
|
7
7
|
|
|
@@ -35,7 +35,7 @@ GEM
|
|
|
35
35
|
rake (13.0.6)
|
|
36
36
|
regexp_parser (2.6.1)
|
|
37
37
|
rexml (3.2.5)
|
|
38
|
-
rubocop (1.
|
|
38
|
+
rubocop (1.41.1)
|
|
39
39
|
json (~> 2.3)
|
|
40
40
|
parallel (~> 1.10)
|
|
41
41
|
parser (>= 3.1.2.1)
|
data/README.md
CHANGED
|
@@ -134,6 +134,45 @@ sub_task.hierarchy_parent == task
|
|
|
134
134
|
sub_task.hierarchy_ancestors_path == 'Project|xxxxxx/Task|yyyyyy/Task|zzzzzz'
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
### Core functionality
|
|
138
|
+
|
|
139
|
+
The core methods that are of interest are the following:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
project.hierarchy_ancestors
|
|
143
|
+
project.hierarchy_parent
|
|
144
|
+
project.hierarchy_siblings
|
|
145
|
+
project.hierarchy_children
|
|
146
|
+
project.hierarchy_descendants
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The major distinction for what is returned is whether you are querying "up the hierarchy" or "down the hierarchy". As there is only 1 path up the hierchy to get to the root, the return values of `hierarchy_ancestors` is a list and `hierarchy_parent` is a single object. However, traversing down the list is a little more tricky as there are various models and potential paths to get all the way do to the leaves. As such, for all methods at the same level or going down the tree (`hierarchy_siblings`, `hierarchy_children`, and `hierarchy_descendants`), the return value is a hash that has the model class as the key, and either a `ActiveRecord::Relation` or a list as the value. For example, for a Project model that has tasks and milestones as descendants, the return value might be something like
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
{
|
|
153
|
+
'Task': [all descendant tasks starting at the project]
|
|
154
|
+
'Milestone': [all descendant milestones starting at the project]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
Given the architecture of this library, this is the most efficient way to return all objects with as few queries as possible.
|
|
158
|
+
|
|
159
|
+
#### Limiting the objects returned
|
|
160
|
+
|
|
161
|
+
All of the methods (except `hierarchy_parent`) take a `models` paramter that can be used to limit the results returned. The potential values are
|
|
162
|
+
|
|
163
|
+
* `:all` (default): Return all objects regardless of type
|
|
164
|
+
* `:this`: Return only objects of the SAME time as the current object
|
|
165
|
+
* An array of models of interest: Return only the objects of the type(s) that are specified (e.g. [`Project`] or [`Project`, `Task`]). The models can be passed either as class objects or a string that can be turned into a class object via `safe_constantize`.
|
|
166
|
+
|
|
167
|
+
There are times when we only need to get the siblings/children/descendants of one type and having a hash returned is a little cumbersome. To deal with this case, you can pass `compact: true` as a parameter and it will return just single result not as a hash. For example:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
# Returns as a hash of the form `{Task: [..all descendants..]}`
|
|
171
|
+
project.hierarch_descendants(models: ['Task'])
|
|
172
|
+
|
|
173
|
+
# Returns just the result: `[..all descendants..]`
|
|
174
|
+
project.hierarch_descendants(models: ['Task'], compact: true)
|
|
175
|
+
```
|
|
137
176
|
### Working with siblings and descendants of an object
|
|
138
177
|
|
|
139
178
|
Let's continue with our `Project` and `Task` example from above and assume we have the following models:
|
|
@@ -199,7 +238,7 @@ However there are times when we need to manually add a child relation to be insp
|
|
|
199
238
|
|
|
200
239
|
```ruby
|
|
201
240
|
class SomeObject
|
|
202
|
-
include
|
|
241
|
+
include Hierarchable
|
|
203
242
|
hierarched parent_source: :parent,
|
|
204
243
|
additional_descendant_associations: [:some_association]
|
|
205
244
|
end
|
|
@@ -209,7 +248,7 @@ There may also be a case when we want exact control over what associations that
|
|
|
209
248
|
|
|
210
249
|
```ruby
|
|
211
250
|
class SomeObject
|
|
212
|
-
include
|
|
251
|
+
include Hierarchable
|
|
213
252
|
hierarched parent_source: :parent,
|
|
214
253
|
descendant_associations: [:some_association]
|
|
215
254
|
end
|
|
@@ -240,16 +240,76 @@ module Hierarchable
|
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
models = hierarchy_descendant_associations.map do |association|
|
|
243
|
-
|
|
244
|
-
.reflection
|
|
245
|
-
.class_name
|
|
246
|
-
.safe_constantize
|
|
243
|
+
class_for_association(association)
|
|
247
244
|
end
|
|
248
245
|
|
|
249
246
|
models << self.class if include_self
|
|
250
247
|
models.uniq
|
|
251
248
|
end
|
|
252
249
|
|
|
250
|
+
# Get the children of an object.
|
|
251
|
+
#
|
|
252
|
+
# For a given object type, return all siblings as a hash such that the key
|
|
253
|
+
# is the model and the value is the list of siblings of that model.
|
|
254
|
+
#
|
|
255
|
+
# If the `models` parameter is `:all` (default), then the result
|
|
256
|
+
# will contain objects of different types. E.g. if we have a Project,
|
|
257
|
+
# Task, and a Comment, the siblings of a Task may include both Tasks and
|
|
258
|
+
# Comments. If you only need this one particular model's data, then
|
|
259
|
+
# set `models` to `:this`. If you want to specify a specific list of models
|
|
260
|
+
# then that can be passed as a list (e.g. [MyModel1, MyModel2])
|
|
261
|
+
#
|
|
262
|
+
# The `include_self` parameter can be set to decide where to start the
|
|
263
|
+
# the children search. If set to `false` (default), then it will return
|
|
264
|
+
# all models found starting with the for all children. If set to
|
|
265
|
+
# `true`, then it will include the current object's class. Note, this
|
|
266
|
+
# parameter is added here for consistency, but in the case of children,
|
|
267
|
+
# it is unlikely that `include_self` would be set to `true`
|
|
268
|
+
# rubocop:disable Metrics/AbcSize
|
|
269
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
270
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
271
|
+
def hierarchy_children(include_self: false, models: :all, compact: false)
|
|
272
|
+
return {} unless respond_to?(:hierarchy_parent_id)
|
|
273
|
+
|
|
274
|
+
# Convert all of the models to actual classes if they are passed as
|
|
275
|
+
# stings.
|
|
276
|
+
if models.is_a?(Array)
|
|
277
|
+
models = models.map do |model|
|
|
278
|
+
model.is_a?(String) ? model.safe_constantize : model
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
result = {}
|
|
283
|
+
hierarchy_descendant_associations.each do |association|
|
|
284
|
+
model = class_for_association(association)
|
|
285
|
+
|
|
286
|
+
next unless models == :all ||
|
|
287
|
+
(models.is_a?(Array) && models.include?(model)) ||
|
|
288
|
+
(models == :this && instance_of?(model))
|
|
289
|
+
|
|
290
|
+
result[model.to_s] = public_send(association)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# If we want to include self, we need to do some extra work
|
|
294
|
+
if include_self
|
|
295
|
+
if result.key?(self.class.to_s)
|
|
296
|
+
result[self.class.to_s] = \
|
|
297
|
+
result[self.class.to_s].or(self.class.where(id:))
|
|
298
|
+
elsif models == :all ||
|
|
299
|
+
models == :this ||
|
|
300
|
+
(models.is_a?(Array) && models.include?(self.class))
|
|
301
|
+
result[self.class.to_s] = [self]
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Compact the results if necessary# Compact the results if necessary
|
|
306
|
+
_, result = result.first if result.size == 1 && compact
|
|
307
|
+
result
|
|
308
|
+
end
|
|
309
|
+
# rubocop:enable Metrics/AbcSize
|
|
310
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
311
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
312
|
+
|
|
253
313
|
# Get all of the sibling models
|
|
254
314
|
#
|
|
255
315
|
# The `include_self` parameter can be set to decide what to include in the
|
|
@@ -265,10 +325,7 @@ module Hierarchable
|
|
|
265
325
|
models.uniq
|
|
266
326
|
end
|
|
267
327
|
|
|
268
|
-
# Get siblings of
|
|
269
|
-
#
|
|
270
|
-
# For a given object type, return all siblings as a hash such that the key
|
|
271
|
-
# is the model and the value is the list of siblings of that model.
|
|
328
|
+
# Get siblings of an object.
|
|
272
329
|
#
|
|
273
330
|
# If the `models` parameter is `:all` (default), then the result
|
|
274
331
|
# will contain objects of different types. E.g. if we have a Project,
|
|
@@ -276,7 +333,9 @@ module Hierarchable
|
|
|
276
333
|
# Comments. If you only need this one particular model's data, then
|
|
277
334
|
# set `models` to `:this`. If you want to specify a specific list of models
|
|
278
335
|
# then that can be passed as a list (e.g. [MyModel1, MyModel2])
|
|
279
|
-
|
|
336
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
337
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
338
|
+
def hierarchy_siblings(include_self: false, models: :all, compact: false)
|
|
280
339
|
return {} unless respond_to?(:hierarchy_parent_id)
|
|
281
340
|
|
|
282
341
|
models = case models
|
|
@@ -290,15 +349,21 @@ module Hierarchable
|
|
|
290
349
|
|
|
291
350
|
result = {}
|
|
292
351
|
models.each do |model|
|
|
352
|
+
model = model.safe_constantize if model.is_a?(String)
|
|
293
353
|
query = model.where(
|
|
294
354
|
hierarchy_parent_type: public_send(:hierarchy_parent_type),
|
|
295
355
|
hierarchy_parent_id: public_send(:hierarchy_parent_id)
|
|
296
356
|
)
|
|
297
357
|
query = query.where.not(id:) if model == self.class && !include_self
|
|
298
|
-
result[model] = query
|
|
358
|
+
result[model.to_s] = query
|
|
299
359
|
end
|
|
360
|
+
|
|
361
|
+
# Compact the results if necessary
|
|
362
|
+
_, result = result.first if result.size == 1 && compact
|
|
300
363
|
result
|
|
301
364
|
end
|
|
365
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
366
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
302
367
|
|
|
303
368
|
# Get all of the descendant models for objects that are descendants of
|
|
304
369
|
# the current one.
|
|
@@ -331,6 +396,7 @@ module Hierarchable
|
|
|
331
396
|
until models_to_analyze.empty?
|
|
332
397
|
|
|
333
398
|
klass = models_to_analyze.pop
|
|
399
|
+
next unless klass
|
|
334
400
|
next if models.include?(klass)
|
|
335
401
|
|
|
336
402
|
obj = klass.new
|
|
@@ -360,9 +426,10 @@ module Hierarchable
|
|
|
360
426
|
# Comments. If you only need this one particular model's data, then
|
|
361
427
|
# set `models` to `:this`. If you want to specify a specific list of models
|
|
362
428
|
# then that can be passed as a list (e.g. [MyModel1, MyModel2])
|
|
429
|
+
# rubocop:disable Metrics/AbcSize
|
|
363
430
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
364
431
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
365
|
-
def hierarchy_descendants(include_self: false, models: :all)
|
|
432
|
+
def hierarchy_descendants(include_self: false, models: :all, compact: false)
|
|
366
433
|
return {} unless respond_to?(:hierarchy_ancestors_path)
|
|
367
434
|
|
|
368
435
|
models = case models
|
|
@@ -376,7 +443,13 @@ module Hierarchable
|
|
|
376
443
|
|
|
377
444
|
result = {}
|
|
378
445
|
models.each do |model|
|
|
446
|
+
model = model.safe_constantize if model.is_a?(String)
|
|
379
447
|
query = if hierarchy_root?
|
|
448
|
+
# If it's the root, we need to base the query based on the
|
|
449
|
+
# hierarchy_root attribute since the ancestor_path will be
|
|
450
|
+
# empty for a root node. See the README for the explanation
|
|
451
|
+
# as to why the root node has values set to nil and the
|
|
452
|
+
# path as the empty string.
|
|
380
453
|
model.where(
|
|
381
454
|
hierarchy_root_type: self.class.name,
|
|
382
455
|
hierarchy_root_id: id
|
|
@@ -388,6 +461,9 @@ module Hierarchable
|
|
|
388
461
|
"#{model.sanitize_sql_like(path)}%"
|
|
389
462
|
)
|
|
390
463
|
end
|
|
464
|
+
|
|
465
|
+
# Make sure to include/exlude the current object depending on what the
|
|
466
|
+
# user wants
|
|
391
467
|
if model == self.class
|
|
392
468
|
query = if include_self
|
|
393
469
|
query.or(model.where(id:))
|
|
@@ -395,10 +471,14 @@ module Hierarchable
|
|
|
395
471
|
query.where.not(id:)
|
|
396
472
|
end
|
|
397
473
|
end
|
|
398
|
-
result[model] = query
|
|
474
|
+
result[model.to_s] = query
|
|
399
475
|
end
|
|
476
|
+
|
|
477
|
+
# Compact the results if necessary
|
|
478
|
+
_, result = result.first if result.size == 1 && compact
|
|
400
479
|
result
|
|
401
480
|
end
|
|
481
|
+
# rubocop:enable Metrics/AbcSize
|
|
402
482
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
403
483
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
404
484
|
|
|
@@ -435,7 +515,7 @@ module Hierarchable
|
|
|
435
515
|
# also add in the one provided.
|
|
436
516
|
#
|
|
437
517
|
# class A
|
|
438
|
-
# include
|
|
518
|
+
# include Hierarchable
|
|
439
519
|
# hierarched parent_source: :parent,
|
|
440
520
|
# additional_descendant_associations: [:some_association]
|
|
441
521
|
# end
|
|
@@ -444,7 +524,7 @@ module Hierarchable
|
|
|
444
524
|
# that should be used. In that case, we can specify it like this:
|
|
445
525
|
#
|
|
446
526
|
# class A
|
|
447
|
-
# include
|
|
527
|
+
# include Hierarchable
|
|
448
528
|
# hierarched parent_source: :parent,
|
|
449
529
|
# descendant_associations: [:some_association]
|
|
450
530
|
# end
|
|
@@ -603,11 +683,15 @@ module Hierarchable
|
|
|
603
683
|
def hierarchy_parent_changed?
|
|
604
684
|
# FIXME: We need to figure out how to deal with updating the
|
|
605
685
|
# object_hierarchy_ancestry_path, object_hierarchy_full_path, etc.,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
686
|
+
return true unless persisted?
|
|
687
|
+
|
|
688
|
+
source = hierarchy_parent_source
|
|
689
|
+
return false if source.blank?
|
|
690
|
+
|
|
691
|
+
changed_method = "#{source}_id_changed?"
|
|
692
|
+
public_send(changed_method) if respond_to?(changed_method)
|
|
693
|
+
|
|
694
|
+
send(source).id == hierarchy_parent_id
|
|
611
695
|
end
|
|
612
696
|
|
|
613
697
|
# Update the hierarchy_ancestors_path if the hierarchy has changed.
|
|
@@ -615,4 +699,11 @@ module Hierarchable
|
|
|
615
699
|
set_hierarchy_ancestors_path
|
|
616
700
|
end
|
|
617
701
|
end
|
|
702
|
+
|
|
703
|
+
# Get the class that is associated with a given association
|
|
704
|
+
def class_for_association(association)
|
|
705
|
+
self.association(association)
|
|
706
|
+
.reflection
|
|
707
|
+
.klass
|
|
708
|
+
end
|
|
618
709
|
end
|
data/lib/hierarchable/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hierarchable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick R. Schmid
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2023-01-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|