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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f108839bd5cf2491856dfdcc1af95e0d50204f32e467c22bb3f4925d0028f5be
4
- data.tar.gz: 2379a67ad164232c8efb8c33f8ca6117c6e51387feb39585b4e114ce16542736
3
+ metadata.gz: 77318268a18001072d590f7ad65524e5f608923cea6c6e757946ad164862518b
4
+ data.tar.gz: 7740d0be435b42dd701fc626eaa706b58c50688c0bb7639da9caf8b5864d3f3b
5
5
  SHA512:
6
- metadata.gz: 88822385da9430f6183eafbc4873a7b3009461d733768453212c3abaf615bca2e318da1c36ac3fb9a4d46bdb072fcd07655069a9148a314212915e205f34baa0
7
- data.tar.gz: bc9b0a309a1e31d7da843ff2ed5f1e75384f9812f63bde5d759018f72de51ebab45e8ea32d45f79cd7f12451d56b439bb69b32b2af0cbc06ee62eeab17ca7d44
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.2.1)
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.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:
@@ -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 Hierarched
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 Hierarched
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
- 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.
@@ -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 Hierarched
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 Hierarched
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
- if hierarchy_parent_source.present?
607
- public_send("#{hierarchy_parent_source}_id_changed?")
608
- else
609
- false
610
- end
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
@@ -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.1'
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.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: 2022-12-23 00:00:00.000000000 Z
11
+ date: 2023-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler