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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f108839bd5cf2491856dfdcc1af95e0d50204f32e467c22bb3f4925d0028f5be
4
- data.tar.gz: 2379a67ad164232c8efb8c33f8ca6117c6e51387feb39585b4e114ce16542736
3
+ metadata.gz: ac67b8aa97065b5917fe0a13e8016d14047cac537bcbb74f8d2910614d86d0da
4
+ data.tar.gz: effb465602763db73be3f247daf54fe523eb3fad1ac489fe75df26f08fd4df0b
5
5
  SHA512:
6
- metadata.gz: 88822385da9430f6183eafbc4873a7b3009461d733768453212c3abaf615bca2e318da1c36ac3fb9a4d46bdb072fcd07655069a9148a314212915e205f34baa0
7
- data.tar.gz: bc9b0a309a1e31d7da843ff2ed5f1e75384f9812f63bde5d759018f72de51ebab45e8ea32d45f79cd7f12451d56b439bb69b32b2af0cbc06ee62eeab17ca7d44
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.2.1)
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.40.0)
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
- self.association(association)
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 the same type for an object.
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
- def hierarchy_siblings(include_self: false, models: :all)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hierarchable
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
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.2.1
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-23 00:00:00.000000000 Z
11
+ date: 2022-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler