hierarchable 0.2.1 → 0.3.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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +39 -0
- data/lib/hierarchable/hierarchable.rb +99 -12
- 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: ac67b8aa97065b5917fe0a13e8016d14047cac537bcbb74f8d2910614d86d0da
|
|
4
|
+
data.tar.gz: effb465602763db73be3f247daf54fe523eb3fad1ac489fe75df26f08fd4df0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f873a3c653dd0eff928657715c49e71eead1080a9798ef7fb97b6d9df296fa4eb686f1335b2e2ba8d1f44af16065246a2918dd864e2b60b29542dcb0772361cc
|
|
7
|
+
data.tar.gz: 0be6318e611e86efec91acb95fb4225c653d43b519dece0444c15eeddd3b58b3aff9815dfaebd013f1aec9d1da64befac6fc23fe30fdc00a85b37abd3fc9ddb2
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
hierarchable (0.
|
|
4
|
+
hierarchable (0.3.0)
|
|
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:
|
|
@@ -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.
|
|
@@ -360,9 +425,10 @@ module Hierarchable
|
|
|
360
425
|
# Comments. If you only need this one particular model's data, then
|
|
361
426
|
# set `models` to `:this`. If you want to specify a specific list of models
|
|
362
427
|
# then that can be passed as a list (e.g. [MyModel1, MyModel2])
|
|
428
|
+
# rubocop:disable Metrics/AbcSize
|
|
363
429
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
364
430
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
365
|
-
def hierarchy_descendants(include_self: false, models: :all)
|
|
431
|
+
def hierarchy_descendants(include_self: false, models: :all, compact: false)
|
|
366
432
|
return {} unless respond_to?(:hierarchy_ancestors_path)
|
|
367
433
|
|
|
368
434
|
models = case models
|
|
@@ -376,7 +442,13 @@ module Hierarchable
|
|
|
376
442
|
|
|
377
443
|
result = {}
|
|
378
444
|
models.each do |model|
|
|
445
|
+
model = model.safe_constantize if model.is_a?(String)
|
|
379
446
|
query = if hierarchy_root?
|
|
447
|
+
# If it's the root, we need to base the query based on the
|
|
448
|
+
# hierarchy_root attribute since the ancestor_path will be
|
|
449
|
+
# empty for a root node. See the README for the explanation
|
|
450
|
+
# as to why the root node has values set to nil and the
|
|
451
|
+
# path as the empty string.
|
|
380
452
|
model.where(
|
|
381
453
|
hierarchy_root_type: self.class.name,
|
|
382
454
|
hierarchy_root_id: id
|
|
@@ -388,6 +460,9 @@ module Hierarchable
|
|
|
388
460
|
"#{model.sanitize_sql_like(path)}%"
|
|
389
461
|
)
|
|
390
462
|
end
|
|
463
|
+
|
|
464
|
+
# Make sure to include/exlude the current object depending on what the
|
|
465
|
+
# user wants
|
|
391
466
|
if model == self.class
|
|
392
467
|
query = if include_self
|
|
393
468
|
query.or(model.where(id:))
|
|
@@ -395,10 +470,14 @@ module Hierarchable
|
|
|
395
470
|
query.where.not(id:)
|
|
396
471
|
end
|
|
397
472
|
end
|
|
398
|
-
result[model] = query
|
|
473
|
+
result[model.to_s] = query
|
|
399
474
|
end
|
|
475
|
+
|
|
476
|
+
# Compact the results if necessary
|
|
477
|
+
_, result = result.first if result.size == 1 && compact
|
|
400
478
|
result
|
|
401
479
|
end
|
|
480
|
+
# rubocop:enable Metrics/AbcSize
|
|
402
481
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
403
482
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
404
483
|
|
|
@@ -615,4 +694,12 @@ module Hierarchable
|
|
|
615
694
|
set_hierarchy_ancestors_path
|
|
616
695
|
end
|
|
617
696
|
end
|
|
697
|
+
|
|
698
|
+
# Get the class that is associated with a given association
|
|
699
|
+
def class_for_association(association)
|
|
700
|
+
self.association(association)
|
|
701
|
+
.reflection
|
|
702
|
+
.class_name
|
|
703
|
+
.safe_constantize
|
|
704
|
+
end
|
|
618
705
|
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.0
|
|
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: 2022-12-
|
|
11
|
+
date: 2022-12-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|