hierarchable 0.1.0 → 0.2.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: c927f016abf308f6c0128f2162908afccb89f55b95495b3499ecfa6a6acdec56
4
- data.tar.gz: 85724a8ec5e256a5251241f7dc5ef8539c2a041bf683834a6ac0081f02efd922
3
+ metadata.gz: f108839bd5cf2491856dfdcc1af95e0d50204f32e467c22bb3f4925d0028f5be
4
+ data.tar.gz: 2379a67ad164232c8efb8c33f8ca6117c6e51387feb39585b4e114ce16542736
5
5
  SHA512:
6
- metadata.gz: 28f8b977db00b49eba24c634bcd19381fa631b519c1c371a4b8fdba15b9ac7475ce638a8e0f87c46f28caf1247abc783de1359d999f6f8343bd1d181f48548f4
7
- data.tar.gz: 7515ea80776d9b60856f91374cedf3d50bfec3f32d572e06bdada94a72a64d64f940ce52b7fd56b1e625ab467885553d6b077d612ad49862f51106e27a6ea4de
6
+ metadata.gz: 88822385da9430f6183eafbc4873a7b3009461d733768453212c3abaf615bca2e318da1c36ac3fb9a4d46bdb072fcd07655069a9148a314212915e205f34baa0
7
+ data.tar.gz: bc9b0a309a1e31d7da843ff2ed5f1e75384f9812f63bde5d759018f72de51ebab45e8ea32d45f79cd7f12451d56b439bb69b32b2af0cbc06ee62eeab17ca7d44
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hierarchable (0.1.0)
4
+ hierarchable (0.2.1)
5
5
  activerecord (> 4.2.0)
6
6
  activesupport (> 4.2.0)
7
7
 
data/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  [![CircleCI](https://dl.circleci.com/status-badge/img/gh/prschmid/hierarchable/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/prschmid/hierarchable/tree/main)
4
4
 
5
- Cross model hierarchical (parent, child, sibling) relationship between ActiveRecord models.
5
+ A simple way to define cross model hierarchical (parent, child, sibling) relationships between ActiveRecord models.
6
+
7
+ The aim of this library is to efficiently create and store the ancestors of an object so that it is easy to generate things like breadcrumbs that require information about an object's ancestors that may span multiple models (e.g. `Project` and `Task`). It is designed in such a way that each object contains the ancestry information and that no joins need to be made to a separate table to get this ancestry information.
6
8
 
7
9
  ## Installation
8
10
 
@@ -43,28 +45,35 @@ t.references :hierarchy_parent,
43
45
  t.string :hierarchy_ancestors_path, index: true
44
46
  ```
45
47
 
46
- The `hierarchy_ancestors_path` column does contain all of the information that is in the `hierarchy_root` and `hierarchy_parent` columns, but those two columns are created for more efficient querying as the direct parent and the root are the most frequent parts of the hierarchy that are needed.
48
+ If you aren't using UUIDs, then simply omit the `type: :uuid` from the two `references` definitions.
49
+
50
+ Note, the `hierarchy_ancestors_path` column does contain all of the information that is in the `hierarchy_root` and `hierarchy_parent` columns, but those two columns are created for more efficient querying as the direct parent and the root are the most frequent parts of the hierarchy that are needed.
47
51
 
48
52
  ## Usage
49
53
 
54
+ ### Getting Started
55
+
50
56
  We will describe the usage using a simplistic Project and Task analogy where we assume that a Project can have many tasks. Given a class `Project` we can set it up as follows
51
57
 
52
58
  ```ruby
53
59
  class Project
54
60
  include Hierarchable
55
61
  hierarchable
62
+ # If desired, could explicitly setting the parent source to `nil`, but this is
63
+ # the same "under the hood"
64
+ # hierarchable parent_source: nil
56
65
  end
57
66
  ```
58
67
 
59
- This will set up the `Project` as the root of the hierarchy. This means that when we query for its root or parent, it will return "self". I.e.
68
+ This will set up the `Project` as the root of the hierarchy. When a `Project` model is saved, it will not have any values for the hierarchy_root, hierarchy_parent, or hierarchy_ancestors_path. This is because for the root item as we are not guaranteed to have an ID for the object until after it is saved, and so there is no way for us to set these values in a consistent way across different use cases. This doesn't affect any of the usage of the library, it's just something to keep in mind.
60
69
 
61
70
  ```ruby
62
71
  project = Project.create!
63
72
 
64
- # These will be true (assuming the the ID of the project is the UUID xxxxxxxx-...)
65
- project.hierarchy_root == project
66
- project.hierarchy_parent == project
67
- project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
73
+ # These will be true.
74
+ project.hierarchy_root == nil
75
+ project.hierarchy_parent == nil
76
+ project.hierarchy_ancestors_path == ''
68
77
  ```
69
78
 
70
79
  Now that we have a project configured, we can add tasks that have projects as a parent.
@@ -84,13 +93,11 @@ This will configure the hierarchy to look at the project association and use tha
84
93
  project = Project.create!
85
94
  task = Task.create!(project: project)
86
95
 
87
- # These will be true
96
+ # These will be true (assuming that the xxxxxx and yyyyyy are the IDs for the
97
+ # project and task respectively)
88
98
  task.hierarchy_root == project
89
99
  task.hierarchy_parent == project
90
- project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
91
- task.hierarchy_root == project
92
- task.hierarchy_parent == project
93
- task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
100
+ task.hierarchy_ancestors_path == 'Project|xxxxxx/Task|yyyyyy'
94
101
  ```
95
102
 
96
103
  Now, let's assume that our tasks can also have other Tasks as subtasks. Once we do that, we need to ensure that the parent of a subtask is the task and not the project. For this we, can do something like the following:
@@ -122,17 +129,94 @@ task = Task.create!(project: project)
122
129
  sub_task = Task.create!(project: project, parent_task: task)
123
130
 
124
131
  # These will be true
125
- task.hierarchy_root == project
126
- task.hierarchy_parent == project
127
- project.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
128
- task.hierarchy_root == project
129
- task.hierarchy_parent == project
130
- task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'
131
132
  sub_task.hierarchy_root == project
132
133
  sub_task.hierarchy_parent == task
133
- sub_task.hierarchy_ancestors_path == 'Project|xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/Task|yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/Task|zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz'
134
+ sub_task.hierarchy_ancestors_path == 'Project|xxxxxx/Task|yyyyyy/Task|zzzzzz'
135
+ ```
136
+
137
+ ### Working with siblings and descendants of an object
138
+
139
+ Let's continue with our `Project` and `Task` example from above and assume we have the following models:
140
+
141
+ ```ruby
142
+ class Project
143
+ include Hierarchable
144
+ hierarchable
145
+ end
146
+
147
+ class Task
148
+ include Hierarchable
149
+ hierarchable parent_source: :project
150
+
151
+ belongs_to :project
152
+ end
153
+
154
+ class Milestone
155
+ include Hierarchable
156
+ hierarchable parent_source: :project
157
+
158
+ belongs_to :project
159
+ end
160
+ ```
161
+
162
+ Based on this setup, we can get all siblings and descendants of either a `Project` or `Task` as follows:
163
+
164
+ ```ruby
165
+ project = Project.create!
166
+ task = Task.create!(project: project)
167
+ milestone = Milestone.create!(project: project)
168
+
169
+ # Query for all Project objects that are siblings of this project.
170
+ # Since the project is the root of the hierarchy, this will return no siblings
171
+ project.hierarchy_siblings
172
+
173
+ # Query for all objects (regardless of type) that are descendats of this project
174
+ # In our example, this will return all Tasks and Milestones
175
+ project.hierarchy_descendants
176
+
177
+ # Query for all Project objects that are descendats of this project
178
+ # In our example, this will return no results
179
+ project.hierarchy_descendants(models: :this)
180
+
181
+ # Query for all Task objects that are siblings of this task.
182
+ # This will return all tasks and milestones that are part of the project
183
+ task.hierarchy_siblings
134
184
  ```
135
185
 
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 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.
187
+
188
+ ```ruby
189
+ class Project
190
+ has_many :tasks
191
+ has_many :completed_tasks, -> { completed }, class_name: 'Task'
192
+ has_many :timestamps, class_name: 'MetricLibrary::Timestamp`
193
+ end
194
+ ```
195
+
196
+ In the `Project` model defined above, only the `:tasks` association will be used for finding descendants.
197
+
198
+ 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.
199
+
200
+ ```ruby
201
+ class SomeObject
202
+ include Hierarched
203
+ hierarched parent_source: :parent,
204
+ additional_descendant_associations: [:some_association]
205
+ end
206
+ ```
207
+
208
+ There may also be a case when we want exact control over what associations that should be used. In that case, we can specify it like this:
209
+
210
+ ```ruby
211
+ class SomeObject
212
+ include Hierarched
213
+ hierarched parent_source: :parent,
214
+ descendant_associations: [:some_association]
215
+ end
216
+ ```
217
+
218
+ Note: For the use case that this library was designed (e.g. creating breadcrumbs) this was a limitation that was perfectly acceptible. In the future we may plan to letusers create an optional "ancestry" table to make this more efficient. Once this table exists, inserts and updates will be slower as an extra object will need to be managed, but queries descenants will be improved.
219
+
136
220
  ### Configuring the separators
137
221
 
138
222
  By default the separators to use for the path and records are `/` and `|` respectively. This means that a hierarchy path will look something like
@@ -69,12 +69,17 @@ module Hierarchable
69
69
  HIERARCHABLE_DEFAULT_RECORD_SEPARATOR = '|'
70
70
 
71
71
  class_methods do
72
+ # rubocop:disable Metrics/MethodLength
72
73
  def hierarchable(opts = {})
73
74
  class_attribute :hierarchable_config
74
75
 
75
76
  # Save the configuration
76
77
  self.hierarchable_config = {
77
78
  parent_source: opts.fetch(:parent_source, nil),
79
+ additional_descendant_associations: opts.fetch(
80
+ :additional_descendant_associations, []
81
+ ),
82
+ descendant_associations: opts.fetch(:descendant_associations, nil),
78
83
  path_separator: opts.fetch(
79
84
  :path_separator, HIERARCHABLE_DEFAULT_PATH_SEPARATOR
80
85
  ),
@@ -105,7 +110,7 @@ module Hierarchable
105
110
 
106
111
  before_create :set_hierarchy_ancestors_path
107
112
 
108
- scope :descendants_of,
113
+ scope :hierarchy_descendants_of,
109
114
  lambda { |object|
110
115
  where(
111
116
  'hierarchy_ancestors_path LIKE :hierarchy_ancestors_path',
@@ -113,10 +118,11 @@ module Hierarchable
113
118
  )
114
119
  }
115
120
 
116
- scope :siblings_of,
121
+ scope :hierarchy_siblings_of,
117
122
  lambda { |object|
118
123
  where(
119
- 'hierarchy_parent_type=:parent_type AND hierarchy_parent_id=:parent_id',
124
+ 'hierarchy_parent_type=:parent_type AND ' \
125
+ 'hierarchy_parent_id=:parent_id',
120
126
  parent_type: object.hierarchy_parent.class.name,
121
127
  parent_id: object.hierarchy_parent.id
122
128
  )
@@ -125,9 +131,14 @@ module Hierarchable
125
131
  include InstanceMethods
126
132
  end
127
133
  end
134
+ # rubocop:enable Metrics/MethodLength
128
135
 
129
136
  # Instance methods to include
130
137
  module InstanceMethods
138
+ def hierarchy_root?
139
+ hierarchy_root.nil?
140
+ end
141
+
131
142
  def hierarchy_parent(raw: false)
132
143
  return hierarchy_parent_relationship if raw
133
144
 
@@ -152,6 +163,245 @@ module Hierarchable
152
163
  end
153
164
  end
154
165
 
166
+ # Get all of the ancestors models
167
+ #
168
+ # The `include_self` parameter can be set to decide where to start the
169
+ # the ancestry search. If set to `false` (default), then it will return
170
+ # all models found starting with the parent of this object. If set to
171
+ # `true`, then it will start with the currect object.
172
+ def hierarchy_ancestor_models(include_self: false)
173
+ return [] unless respond_to?(:hierarchy_ancestors_path)
174
+ return include_self ? [self.class] : [] if hierarchy_ancestors_path.blank?
175
+
176
+ models = hierarchy_ancestors_path.split(
177
+ hierarchable_config[:path_separator]
178
+ ).map do |ancestor|
179
+ ancestor_class, = \
180
+ ancestor.split(hierarchable_config[:record_separator])
181
+ ancestor_class.safe_constantize
182
+ end.uniq
183
+
184
+ models << self.class if include_self
185
+ models.uniq
186
+ end
187
+
188
+ # Get ancestors of the same type for an object.
189
+ #
190
+ # Using the `hierarchy_ancestors_path`, this will iteratively get all
191
+ # ancestor objects and return them as a list.
192
+ #
193
+ # If the `models` parameter is `:all` (default), then the result
194
+ # will contain objects of different types. E.g. if we have a Project,
195
+ # Task, and a Comment, the siblings of a Task may include both Tasks and
196
+ # Comments. If you only need this one particular model's data, then
197
+ # set `models` to `:this`. If you want to specify a specific list of models
198
+ # then that can be passed as a list (e.g. [MyModel1, MyModel2])
199
+ # rubocop:disable Metrics/CyclomaticComplexity
200
+ # rubocop:disable Metrics/PerceivedComplexity
201
+ def hierarchy_ancestors(include_self: false, models: :all)
202
+ return [] unless respond_to?(:hierarchy_ancestors_path)
203
+ return include_self ? [self] : [] if hierarchy_ancestors_path.blank?
204
+
205
+ ancestors = hierarchy_ancestors_path.split(
206
+ hierarchable_config[:path_separator]
207
+ ).map do |ancestor|
208
+ ancestor_class, ancestor_id = ancestor.split(
209
+ hierarchable_config[:record_separator]
210
+ )
211
+
212
+ next if ancestor_class != self.class.name && models != :all
213
+ next if models.is_a?(Array) && models.exclude?(ancestor_class)
214
+
215
+ ancestor_class.safe_constantize.find(ancestor_id)
216
+ end
217
+
218
+ ancestors.compact
219
+ ancestors << self if include_self
220
+ ancestors
221
+ end
222
+ # rubocop:enable Metrics/CyclomaticComplexity
223
+ # rubocop:enable Metrics/PerceivedComplexity
224
+
225
+ # Get all of the models of the children that this object could have
226
+ #
227
+ # This is based on the models identified in the
228
+ # `hierarchy_descendant_associations` association
229
+ #
230
+ # The `include_self` parameter can be set to decide where to start the
231
+ # the children search. If set to `false` (default), then it will return
232
+ # all models found starting with the for all children. If set to
233
+ # `true`, then it will include the current object's class. Note, this
234
+ # parameter is added here for consistency, but in the case of children
235
+ # models, it is unlikely that `include_self` would be set to `true`
236
+ def hierarchy_children_models(include_self: false)
237
+ return [] unless respond_to?(:hierarchy_descendant_associations)
238
+ if hierarchy_descendant_associations.blank?
239
+ return include_self ? [self.class] : []
240
+ end
241
+
242
+ models = hierarchy_descendant_associations.map do |association|
243
+ self.association(association)
244
+ .reflection
245
+ .class_name
246
+ .safe_constantize
247
+ end
248
+
249
+ models << self.class if include_self
250
+ models.uniq
251
+ end
252
+
253
+ # Get all of the sibling models
254
+ #
255
+ # The `include_self` parameter can be set to decide what to include in the
256
+ # sibling models search. If set to `false` (default), then it will return
257
+ # all models other models that are siblings of the current object. If set to
258
+ # `true`, then it will also include the current object's class.
259
+ def hierarchy_sibling_models(include_self: false)
260
+ return [] unless respond_to?(:hierarchy_parent)
261
+ return include_self ? [self.class] : [] if hierarchy_parent.blank?
262
+
263
+ models = hierarchy_parent.hierarchy_children_models(include_self: false)
264
+ models << self.class if include_self
265
+ models.uniq
266
+ end
267
+
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.
272
+ #
273
+ # If the `models` parameter is `:all` (default), then the result
274
+ # will contain objects of different types. E.g. if we have a Project,
275
+ # Task, and a Comment, the siblings of a Task may include both Tasks and
276
+ # Comments. If you only need this one particular model's data, then
277
+ # set `models` to `:this`. If you want to specify a specific list of models
278
+ # then that can be passed as a list (e.g. [MyModel1, MyModel2])
279
+ def hierarchy_siblings(include_self: false, models: :all)
280
+ return {} unless respond_to?(:hierarchy_parent_id)
281
+
282
+ models = case models
283
+ when Array
284
+ models
285
+ when :all
286
+ hierarchy_sibling_models(include_self: true)
287
+ else
288
+ [self.class]
289
+ end
290
+
291
+ result = {}
292
+ models.each do |model|
293
+ query = model.where(
294
+ hierarchy_parent_type: public_send(:hierarchy_parent_type),
295
+ hierarchy_parent_id: public_send(:hierarchy_parent_id)
296
+ )
297
+ query = query.where.not(id:) if model == self.class && !include_self
298
+ result[model] = query
299
+ end
300
+ result
301
+ end
302
+
303
+ # Get all of the descendant models for objects that are descendants of
304
+ # the current one.
305
+ #
306
+ # This will make use of the `hierarchy_descendant_associations` to find
307
+ # all models.
308
+ #
309
+ # Unlike `hierarchy_children_models` that only looks at the immediate
310
+ # children of an object, this method will look at all descenants of the
311
+ # current object and find the models. In other words, this will follow
312
+ # all relationships of all children, and those children's children to
313
+ # get all models that could potentially be descendants of the current
314
+ # model.
315
+ #
316
+ # The `include_self` parameter can be set to decide where to start the
317
+ # the descentant search. If set to `false` (default), then it will return
318
+ # all models found starting with the children of this object. If set to
319
+ # `true`, then it will start with the currect object.
320
+ # rubocop:disable Metrics/CyclomaticComplexity
321
+ # rubocop:disable Metrics/PerceivedComplexity
322
+ def hierarchy_descendant_models(include_self: false)
323
+ return [] unless respond_to?(:hierarchy_descendant_associations)
324
+
325
+ if hierarchy_descendant_associations.blank?
326
+ return include_self ? [self.class] : []
327
+ end
328
+
329
+ models = []
330
+ models_to_analyze = [self.class]
331
+ until models_to_analyze.empty?
332
+
333
+ klass = models_to_analyze.pop
334
+ next if models.include?(klass)
335
+
336
+ obj = klass.new
337
+ next unless obj.respond_to?(:hierarchy_descendant_associations)
338
+
339
+ models_to_analyze += obj.hierarchy_children_models(include_self: false)
340
+
341
+ next if klass == self.class && !include_self
342
+
343
+ models << klass
344
+ end
345
+ models.uniq
346
+ end
347
+ # rubocop:enable Metrics/CyclomaticComplexity
348
+ # rubocop:enable Metrics/PerceivedComplexity
349
+
350
+ # Get descendants for an object.
351
+ #
352
+ # The `include_self` parameter can be set to decide where to start the
353
+ # the descentant search. If set to `false` (default), then it will return
354
+ # all models found starting with the children of this object. If set to
355
+ # `true`, then it will start with the currect object.
356
+ #
357
+ # If the `models` parameter is `:all` (default), then the result
358
+ # will contain objects of different types. E.g. if we have a Project,
359
+ # Task, and a Comment, the siblings of a Task may include both Tasks and
360
+ # Comments. If you only need this one particular model's data, then
361
+ # set `models` to `:this`. If you want to specify a specific list of models
362
+ # then that can be passed as a list (e.g. [MyModel1, MyModel2])
363
+ # rubocop:disable Metrics/CyclomaticComplexity
364
+ # rubocop:disable Metrics/PerceivedComplexity
365
+ def hierarchy_descendants(include_self: false, models: :all)
366
+ return {} unless respond_to?(:hierarchy_ancestors_path)
367
+
368
+ models = case models
369
+ when Array
370
+ models
371
+ when :all
372
+ hierarchy_descendant_models(include_self: true)
373
+ else
374
+ [self.class]
375
+ end
376
+
377
+ result = {}
378
+ models.each do |model|
379
+ query = if hierarchy_root?
380
+ model.where(
381
+ hierarchy_root_type: self.class.name,
382
+ hierarchy_root_id: id
383
+ )
384
+ else
385
+ path = public_send(:hierarchy_full_path)
386
+ model.where(
387
+ 'hierarchy_ancestors_path LIKE ?',
388
+ "#{model.sanitize_sql_like(path)}%"
389
+ )
390
+ end
391
+ if model == self.class
392
+ query = if include_self
393
+ query.or(model.where(id:))
394
+ else
395
+ query.where.not(id:)
396
+ end
397
+ end
398
+ result[model] = query
399
+ end
400
+ result
401
+ end
402
+ # rubocop:enable Metrics/CyclomaticComplexity
403
+ # rubocop:enable Metrics/PerceivedComplexity
404
+
155
405
  # Return the attribute name that links this object to its parent.
156
406
  #
157
407
  # This should return the name of the attribute/relation/etc either as a
@@ -168,6 +418,53 @@ module Hierarchable
168
418
  source.respond_to?(:call) ? source.call(self) : source
169
419
  end
170
420
 
421
+ # Return all of the `has_many` association names this class class has as a
422
+ # list of symbols.
423
+ #
424
+ # In order to be safe and not return potential duplicate associations,
425
+ # the only associations that are automatically
426
+ # detected are the ones that are the pluralized form of the model name.
427
+ # For example, if a model as the association `has_many :tasks`, there
428
+ # will need to be a Task model for this association to be kept.
429
+ #
430
+ # If there are some associations that need to be manually
431
+ # added, one simply needs to specify them when setting up the model.
432
+ #
433
+ # The most common case is if we want to specify additional associations.
434
+ # This will take all of the associations that can be auto-detected and
435
+ # also add in the one provided.
436
+ #
437
+ # class A
438
+ # include Hierarched
439
+ # hierarched parent_source: :parent,
440
+ # additional_descendant_associations: [:some_association]
441
+ # end
442
+ #
443
+ # There may also be a case when we want exact control over what associations
444
+ # that should be used. In that case, we can specify it like this:
445
+ #
446
+ # class A
447
+ # include Hierarched
448
+ # hierarched parent_source: :parent,
449
+ # descendant_associations: [:some_association]
450
+ # end
451
+ def hierarchy_descendant_associations
452
+ if hierarchable_config[:descendant_associations].present?
453
+ return hierarchable_config[:descendant_associations]
454
+ end
455
+
456
+ associations = \
457
+ self.class
458
+ .reflect_on_all_associations(:has_many)
459
+ .reject do |a|
460
+ a.name.to_s.singularize.camelcase.safe_constantize.nil?
461
+ end
462
+ .reject(&:through_reflection?)
463
+ .map(&:name)
464
+ associations += hierarchable_config[:additional_descendant_associations]
465
+ associations.uniq
466
+ end
467
+
171
468
  # Return the string representation of the current object in the format when
172
469
  # used as part of a hierarchy.
173
470
  #
@@ -199,7 +496,6 @@ module Hierarchable
199
496
  end
200
497
 
201
498
  # Return hierarchy path for given list of objects
202
-
203
499
  def hierarchy_path_for(objects)
204
500
  return '' if objects.blank?
205
501
 
@@ -241,89 +537,6 @@ module Hierarchable
241
537
  path
242
538
  end
243
539
 
244
- # Get ancestors of the same type for an object.
245
- #
246
- # For a given object type, return all ancestors that have the same type.
247
- # Note, since ancestors may be of different types, this may skip parts
248
- # of the hierarchy if the particular ancestor happens to be of a different
249
- # type.
250
- def ancestors
251
- return [] if !respond_to?(:hierarchy_ancestors_path) ||
252
- hierarchy_ancestors_path.blank?
253
-
254
- a = hierarchy_ancestors_path.split(
255
- hierarchable_config[:path_separator]
256
- ).map do |ancestor|
257
- ancestor_class, ancestor_id = ancestor.split(
258
- hierarchable_config[:record_separator]
259
- )
260
-
261
- if ancestor_class == self.class.name
262
- ancestor_class.safe_constantize.find(ancestor_id)
263
- end
264
- end
265
- a.compact
266
- end
267
-
268
- # Return the list of all ancestor objects for the current object
269
- #
270
- # Using the `hierarchy_ancestors_path`, this will iteratively get all
271
- # ancestor objects and return them as a list.
272
- #
273
- # As there may be ancestors of different types, this is not a single query
274
- # and may return things of many different types. E.g. if we have a Project,
275
- # Task, and a Comment, the ancestors of a coment may be the Task and the
276
- # Project.
277
- def all_ancestors
278
- return [] if !respond_to?(:hierarchy_ancestors_path) ||
279
- hierarchy_ancestors_path.blank?
280
-
281
- hierarchy_ancestors_path.split(
282
- hierarchable_config[:path_separator]
283
- ).map do |ancestor|
284
- ancestor_class, ancestor_id = ancestor.split(
285
- hierarchable_config[:record_separator]
286
- )
287
- ancestor_class.safe_constantize.find(ancestor_id)
288
- end
289
- end
290
-
291
- # Get siblings of the same type for an object.
292
- #
293
- # For a given object type, return all siblings. Note, this DOES NOT return
294
- # siblings of different types and those need to be queried separetly.
295
- # equivalent to c.hierarchy_parent.children
296
- #
297
- # Params:
298
- # +include_self+:: Whether or not to include self in the list.
299
- # Default is true
300
- def siblings(include_self: true)
301
- # The method should always return relation, not an Array sometimes and
302
- # Relation the other
303
- return self.class.none unless respond_to?(:hierarchy_parent_id)
304
-
305
- query = self.class.where(
306
- hierarchy_parent_type: public_send(:hierarchy_parent_type),
307
- hierarchy_parent_id: public_send(:hierarchy_parent_id)
308
- )
309
- query = query.where.not(id:) unless include_self
310
- query
311
- end
312
-
313
- # Get all siblings of this object regardless of object type.
314
- #
315
- # This has yet to be implemented and would likely require a separate join
316
- # table that has all of the data across all tables linked to the particular
317
- # parent. I.e. a simple table that has parent, child in it that we could
318
- # use to query.
319
- #
320
- # Params:
321
- # +include_self+:: Whether or not to include self in the list.
322
- # Default is true
323
- def all_siblings
324
- raise NotImplementedError
325
- end
326
-
327
540
  protected
328
541
 
329
542
  # Set the hierarchy_parent of the current object.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hierarchable
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.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.1.0
4
+ version: 0.2.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-22 00:00:00.000000000 Z
11
+ date: 2022-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler