hierarchable 0.1.0 → 0.2.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: c927f016abf308f6c0128f2162908afccb89f55b95495b3499ecfa6a6acdec56
4
- data.tar.gz: 85724a8ec5e256a5251241f7dc5ef8539c2a041bf683834a6ac0081f02efd922
3
+ metadata.gz: 2cbb9e575443a02be4dff5614306785def4eecb5a7738af39261a9001de4739c
4
+ data.tar.gz: fb740c37795bafefbb095ce5e0bf1a0cf42b80771976f79dda4919b87bd4b3da
5
5
  SHA512:
6
- metadata.gz: 28f8b977db00b49eba24c634bcd19381fa631b519c1c371a4b8fdba15b9ac7475ce638a8e0f87c46f28caf1247abc783de1359d999f6f8343bd1d181f48548f4
7
- data.tar.gz: 7515ea80776d9b60856f91374cedf3d50bfec3f32d572e06bdada94a72a64d64f940ce52b7fd56b1e625ab467885553d6b077d612ad49862f51106e27a6ea4de
6
+ metadata.gz: b63216b7d4c1887f8787f3775ab4f4781be9103723db3c132a1c5b87ed504ebe97129cec288662aff04e9d7755a5f567e6eb3a54b8a4a29d88fd5fb9f521582f
7
+ data.tar.gz: 71dee24abbeced1a0b5f13d0f8f8b0aa8bc6863f77a3f3c47e6286d7dfd77a1a4fbf8e8bb68c8dab95e9d17be3d28ddbcaa6d9ee55d34d658f6fba9db0ef97c5
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.0)
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,84 @@ 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 most cases these relations can be inferred correctly by getting all of the `has_many` relationships that a model has defined. 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.
187
+
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.
189
+
190
+ ```ruby
191
+ class SomeObject
192
+ include Hierarched
193
+ hierarched parent_source: :parent,
194
+ additional_descendant_associations: [:some_association]
195
+ end
196
+ ```
197
+
198
+ 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:
199
+
200
+ ```ruby
201
+ class SomeObject
202
+ include Hierarched
203
+ hierarched parent_source: :parent,
204
+ descendant_associations: [:some_association]
205
+ end
206
+ ```
207
+
208
+ 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.
209
+
136
210
  ### Configuring the separators
137
211
 
138
212
  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
+ :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_ancestors_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,49 @@ 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
+ # The assumption is that all of the associations we care about for
425
+ # getting descendants can easily be obtained directly from inspecting
426
+ # the class. If there are some associations that need to be manually
427
+ # added, one simply specify them when setting up the model.
428
+ #
429
+ # The most common case is if we want to specify additional associations.
430
+ # This will take all of the associations that can be auto-detected and
431
+ # also add in the one provided.
432
+ #
433
+ # class A
434
+ # include Hierarched
435
+ # hierarched parent_source: :parent,
436
+ # additional_descendant_associations: [:some_association]
437
+ # end
438
+ #
439
+ # There may also be a case when we want exact control over what associations
440
+ # that should be used. In that case, we can specify it like this:
441
+ #
442
+ # class A
443
+ # include Hierarched
444
+ # hierarched parent_source: :parent,
445
+ # descendant_associations: [:some_association]
446
+ # end
447
+ def hierarchy_descendant_associations
448
+ if hierarchable_config[:descendant_associations].present?
449
+ return hierarchable_config[:descendant_associations]
450
+ end
451
+
452
+ associations = \
453
+ self.class
454
+ .reflect_on_all_associations(:has_many)
455
+ .reject do |a|
456
+ a.name.to_s.singularize.camelcase.safe_constantize.nil?
457
+ end
458
+ .reject(&:through_reflection?)
459
+ .map(&:name)
460
+ associations += hierarchable_config[:additional_descendant_associations]
461
+ associations
462
+ end
463
+
171
464
  # Return the string representation of the current object in the format when
172
465
  # used as part of a hierarchy.
173
466
  #
@@ -199,7 +492,6 @@ module Hierarchable
199
492
  end
200
493
 
201
494
  # Return hierarchy path for given list of objects
202
-
203
495
  def hierarchy_path_for(objects)
204
496
  return '' if objects.blank?
205
497
 
@@ -241,89 +533,6 @@ module Hierarchable
241
533
  path
242
534
  end
243
535
 
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
536
  protected
328
537
 
329
538
  # 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.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick R. Schmid