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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +103 -19
- data/lib/hierarchable/hierarchable.rb +300 -87
- 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: f108839bd5cf2491856dfdcc1af95e0d50204f32e467c22bb3f4925d0028f5be
|
|
4
|
+
data.tar.gz: 2379a67ad164232c8efb8c33f8ca6117c6e51387feb39585b4e114ce16542736
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88822385da9430f6183eafbc4873a7b3009461d733768453212c3abaf615bca2e318da1c36ac3fb9a4d46bdb072fcd07655069a9148a314212915e205f34baa0
|
|
7
|
+
data.tar.gz: bc9b0a309a1e31d7da843ff2ed5f1e75384f9812f63bde5d759018f72de51ebab45e8ea32d45f79cd7f12451d56b439bb69b32b2af0cbc06ee62eeab17ca7d44
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://dl.circleci.com/status-badge/redirect/gh/prschmid/hierarchable/tree/main)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
65
|
-
project.hierarchy_root ==
|
|
66
|
-
project.hierarchy_parent ==
|
|
67
|
-
project.hierarchy_ancestors_path == '
|
|
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
|
-
|
|
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|
|
|
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 :
|
|
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 :
|
|
121
|
+
scope :hierarchy_siblings_of,
|
|
117
122
|
lambda { |object|
|
|
118
123
|
where(
|
|
119
|
-
'hierarchy_parent_type=:parent_type AND
|
|
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.
|
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.1
|
|
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-
|
|
11
|
+
date: 2022-12-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|