hierarchable 0.2.0 → 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 +53 -4
- data/lib/hierarchable/hierarchable.rb +111 -20
- 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:
|
|
@@ -183,15 +222,25 @@ project.hierarchy_descendants(models: :this)
|
|
|
183
222
|
task.hierarchy_siblings
|
|
184
223
|
```
|
|
185
224
|
|
|
186
|
-
In order to figure out the potential descendants of an object we need to inspect the object and query all relations to to see if any of those have this object as an ancestor. In
|
|
225
|
+
In order to figure out the potential descendants of an object we need to inspect the object and query all relations to to see if any of those have this object as an ancestor. In many cases these relations can be inferred correctly by getting all of the `has_many` relationships that a model has defined. To be safe and not return potential duplicate associations, the only associations that are automatically detected are the ones that are the pluralized form of the model name.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class Project
|
|
229
|
+
has_many :tasks
|
|
230
|
+
has_many :completed_tasks, -> { completed }, class_name: 'Task'
|
|
231
|
+
has_many :timestamps, class_name: 'MetricLibrary::Timestamp`
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
In the `Project` model defined above, only the `:tasks` association will be used for finding descendants.
|
|
187
236
|
|
|
188
|
-
The most common case is if we want to specify additional associations. This will take all of the associations that can be auto-detected and also add in the one provided.
|
|
237
|
+
However there are times when we need to manually add a child relation to be inspected. This can be done in one of two ways. The most common case is if we want to specify additional associations. This will take all of the associations that can be auto-detected and also add in the one provided.
|
|
189
238
|
|
|
190
239
|
```ruby
|
|
191
240
|
class SomeObject
|
|
192
241
|
include Hierarched
|
|
193
242
|
hierarched parent_source: :parent,
|
|
194
|
-
|
|
243
|
+
additional_descendant_associations: [:some_association]
|
|
195
244
|
end
|
|
196
245
|
```
|
|
197
246
|
|
|
@@ -201,7 +250,7 @@ There may also be a case when we want exact control over what associations that
|
|
|
201
250
|
class SomeObject
|
|
202
251
|
include Hierarched
|
|
203
252
|
hierarched parent_source: :parent,
|
|
204
|
-
|
|
253
|
+
descendant_associations: [:some_association]
|
|
205
254
|
end
|
|
206
255
|
```
|
|
207
256
|
|
|
@@ -77,7 +77,7 @@ module Hierarchable
|
|
|
77
77
|
self.hierarchable_config = {
|
|
78
78
|
parent_source: opts.fetch(:parent_source, nil),
|
|
79
79
|
additional_descendant_associations: opts.fetch(
|
|
80
|
-
:
|
|
80
|
+
:additional_descendant_associations, []
|
|
81
81
|
),
|
|
82
82
|
descendant_associations: opts.fetch(:descendant_associations, nil),
|
|
83
83
|
path_separator: opts.fetch(
|
|
@@ -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,18 +442,27 @@ 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
|
|
383
455
|
)
|
|
384
456
|
else
|
|
385
|
-
path = public_send(:
|
|
457
|
+
path = public_send(:hierarchy_full_path)
|
|
386
458
|
model.where(
|
|
387
459
|
'hierarchy_ancestors_path LIKE ?',
|
|
388
|
-
"#{model.sanitize_sql_like(path)}
|
|
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
|
|
|
@@ -421,10 +500,14 @@ module Hierarchable
|
|
|
421
500
|
# Return all of the `has_many` association names this class class has as a
|
|
422
501
|
# list of symbols.
|
|
423
502
|
#
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
# the
|
|
427
|
-
#
|
|
503
|
+
# In order to be safe and not return potential duplicate associations,
|
|
504
|
+
# the only associations that are automatically
|
|
505
|
+
# detected are the ones that are the pluralized form of the model name.
|
|
506
|
+
# For example, if a model as the association `has_many :tasks`, there
|
|
507
|
+
# will need to be a Task model for this association to be kept.
|
|
508
|
+
#
|
|
509
|
+
# If there are some associations that need to be manually
|
|
510
|
+
# added, one simply needs to specify them when setting up the model.
|
|
428
511
|
#
|
|
429
512
|
# The most common case is if we want to specify additional associations.
|
|
430
513
|
# This will take all of the associations that can be auto-detected and
|
|
@@ -458,7 +541,7 @@ module Hierarchable
|
|
|
458
541
|
.reject(&:through_reflection?)
|
|
459
542
|
.map(&:name)
|
|
460
543
|
associations += hierarchable_config[:additional_descendant_associations]
|
|
461
|
-
associations
|
|
544
|
+
associations.uniq
|
|
462
545
|
end
|
|
463
546
|
|
|
464
547
|
# Return the string representation of the current object in the format when
|
|
@@ -611,4 +694,12 @@ module Hierarchable
|
|
|
611
694
|
set_hierarchy_ancestors_path
|
|
612
695
|
end
|
|
613
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
|
|
614
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
|