/
traversable.rb
377 lines (340 loc) · 12.1 KB
/
traversable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# frozen_string_literal: true
require 'mongoid/fields/validators/macro'
module Mongoid
# Mixin module included in Mongoid::Document to provide behavior
# around traversing the document graph.
module Traversable
extend ActiveSupport::Concern
# Class-level methods for the Traversable behavior.
module ClassMethods
# Determines if the document is a subclass of another document.
#
# @example Check if the document is a subclass.
# Square.hereditary?
#
# @return [ true | false ] True if hereditary, false if not.
def hereditary?
!!(superclass < Mongoid::Document)
end
# When inheriting, we want to copy the fields from the parent class and
# set the on the child to start, mimicking the behavior of the old
# class_inheritable_accessor that was deprecated in Rails edge.
#
# @example Inherit from this class.
# Person.inherited(Doctor)
#
# @param [ Class ] subclass The inheriting class.
#
# rubocop:disable Metrics/AbcSize
def inherited(subclass)
super
@_type = nil
subclass.aliased_fields = aliased_fields.dup
subclass.localized_fields = localized_fields.dup
subclass.fields = fields.dup
subclass.pre_processed_defaults = pre_processed_defaults.dup
subclass.post_processed_defaults = post_processed_defaults.dup
subclass._declared_scopes = Hash.new { |_hash, key| _declared_scopes[key] }
subclass.discriminator_value = subclass.name
# We need to do this here because the discriminator_value method is
# overridden in the subclass above.
subclass.include DiscriminatorRetrieval
# We only need the _type field if inheritance is in play, but need to
# add to the root class as well for backwards compatibility.
return if fields.key?(discriminator_key)
default_proc = -> { self.class.discriminator_value }
field(discriminator_key, default: default_proc, type: String)
end
# rubocop:enable Metrics/AbcSize
end
# `_parent` is intentionally not implemented via attr_accessor because
# of the need to use a double underscore for the instance variable.
# Associations automatically create backing variables prefixed with a
# single underscore, which would conflict with this accessor if a model
# were to declare a `parent` association.
# Retrieves the parent document of this document.
#
# @return [ Mongoid::Document | nil ] the parent document
#
# @api private
def _parent
@__parent || nil
end
# Sets the parent document of this document.
#
# @param [ Mongoid::Document | nil ] document the document to set as
# the parent document.
#
# @returns [ Mongoid::Document ] The parent document.
#
# @api private
def _parent=(document)
@__parent = document
end
# Module used for prepending to the various discriminator_*= methods
#
# @api private
module DiscriminatorAssignment
# Sets the discriminator key.
#
# @param [ String ] value The discriminator key to set.
#
# @api private
# rubocop:disable Metrics/AbcSize
def discriminator_key=(value)
raise Errors::InvalidDiscriminatorKeyTarget.new(self, superclass) if hereditary?
_mongoid_clear_types
if value
Mongoid::Fields::Validators::Macro.validate_field_name(self, value)
value = value.to_s
super
else
# When discriminator key is set to nil, replace the class's definition
# of the discriminator key reader (provided by class_attribute earlier)
# and re-delegate to Mongoid.
class << self
delegate :discriminator_key, to: ::Mongoid
end
end
# This condition checks if the new discriminator key would overwrite
# an existing field.
# This condition also checks if the class has any descendants, because
# if it doesn't then it doesn't need a discriminator key.
return unless !fields.key?(discriminator_key) && !descendants.empty?
default_proc = -> { self.class.discriminator_value }
field(discriminator_key, default: default_proc, type: String)
end
# rubocop:enable Metrics/AbcSize
# Returns the discriminator key.
#
# @return [ String ] The discriminator key.
#
# @api private
def discriminator_value=(value)
value ||= name
_mongoid_clear_types
add_discriminator_mapping(value)
@discriminator_value = value
end
end
# Module used for prepending the discriminator_value method.
#
# A separate module was needed because the subclasses of this class
# need to be manually prepended with the discriminator_value and can't
# rely on being a class_attribute because the .discriminator_value
# method is overridden by every subclass in the inherited method.
#
# @api private
module DiscriminatorRetrieval
# Get the name on the reading side if the discriminator_value is nil
def discriminator_value
@discriminator_value || name
end
end
included do
class_attribute :discriminator_key, instance_accessor: false
class << self
delegate :discriminator_key, to: ::Mongoid
prepend DiscriminatorAssignment
include DiscriminatorRetrieval
# @api private
#
# @return [ Hash<String, Class> ] The current mapping of discriminator_values to classes
attr_accessor :discriminator_mapping
end
# Add a discriminator mapping to the parent class. This mapping is used when
# receiving a document to identify its class.
#
# @param [ String ] value The discriminator_value that was just set
# @param [ Class ] The class the discriminator_value was set on
#
# @api private
def self.add_discriminator_mapping(value, klass = self)
self.discriminator_mapping ||= {}
self.discriminator_mapping[value] = klass
superclass.add_discriminator_mapping(value, klass) if hereditary?
end
# Get the discriminator mapping from the parent class. This method returns nil if there
# is no mapping for the given value.
#
# @param [ String ] value The discriminator_value to retrieve
#
# @return [ Class | nil ] klass The class corresponding to the given discriminator_value. If
# the value is not in the mapping, this method returns nil.
#
# @api private
def self.get_discriminator_mapping(value)
self.discriminator_mapping[value] if self.discriminator_mapping
end
end
# Get all child +Documents+ to this +Document+
#
# @return [ Array<Document> ] All child documents in the hierarchy.
#
# @api private
def _children(reset: false)
# See discussion above for the `_parent` method, as to why the variable
# here needs to have two underscores.
#
# rubocop:disable Naming/MemoizedInstanceVariableName
if reset
@__children = nil
else
@__children ||= collect_children
end
# rubocop:enable Naming/MemoizedInstanceVariableName
end
# Get all descendant +Documents+ of this +Document+ recursively.
# This is used when calling update persistence operations from
# the root document, where changes in the entire tree need to be
# determined. Note that persistence from the embedded documents will
# always be preferred, since they are optimized calls... This operation
# can get expensive in domains with large hierarchies.
#
# @return [ Array<Document> ] All descendant documents in the hierarchy.
#
# @api private
def _descendants(reset: false)
# See discussion above for the `_parent` method, as to why the variable
# here needs to have two underscores.
#
# rubocop:disable Naming/MemoizedInstanceVariableName
if reset
@__descendants = nil
else
@__descendants ||= collect_descendants
end
# rubocop:enable Naming/MemoizedInstanceVariableName
end
# Collect all the children of this document.
#
# @return [ Array<Document> ] The children.
#
# @api private
def collect_children
[].tap do |children|
embedded_relations.each_pair do |name, _association|
without_autobuild do
child = send(name)
children.concat(Array.wrap(child)) if child
end
end
end
end
# Collect all the descendants of this document.
#
# @return [ Array<Document> ] The descendants.
#
# @api private
def collect_descendants
children = []
to_expand = _children
expanded = {}
until to_expand.empty?
expanding = to_expand
to_expand = []
expanding.each do |child|
next if expanded[child]
# Don't mark expanded if _id is nil, since documents are compared by
# their _ids, multiple embedded documents with nil ids will compare
# equally, and some documents will not be expanded.
expanded[child] = true if child._id
children << child
to_expand += child._children
end
end
children
end
# Marks all descendants as being persisted.
#
# @return [ Array<Document> ] The flagged descendants.
def flag_descendants_persisted
_descendants.each do |child|
child.new_record = false
end
end
# Determines if the document is a subclass of another document.
#
# @example Check if the document is a subclass
# Square.new.hereditary?
#
# @return [ true | false ] True if hereditary, false if not.
def hereditary?
self.class.hereditary?
end
# Sets up a child/parent association. This is used for newly created
# objects so they can be properly added to the graph.
#
# @example Set the parent document.
# document.parentize(parent)
#
# @param [ Document ] document The parent document.
#
# @return [ Document ] The parent document.
def parentize(document)
self._parent = document
end
# Remove a child document from this parent. If an embeds one then set to
# nil, otherwise remove from the embeds many.
#
# This is called from the +RemoveEmbedded+ persistence command.
#
# @example Remove the child.
# document.remove_child(child)
#
# @param [ Document ] child The child (embedded) document to remove.
def remove_child(child)
name = child.association_name
if child.embedded_one?
attributes.delete(child._association.store_as)
remove_ivar(name)
else
relation = send(name)
relation._remove(child)
end
end
# After descendants are persisted we can call this to move all their
# changes and flag them as persisted in one call.
#
# @return [ Array<Document> ] The descendants.
def reset_persisted_descendants
_descendants.each do |child|
child.move_changes
child.new_record = false
end
_reset_memoized_descendants!
end
# Resets the memoized descendants on the object. Called internally when an
# embedded array changes size.
#
# @return [ nil ] nil.
#
# @api private
def _reset_memoized_descendants!
_parent&._reset_memoized_descendants!
_children reset: true
_descendants reset: true
end
# Return the root document in the object graph. If the current document
# is the root object in the graph it will return self.
#
# @example Get the root document in the hierarchy.
# document._root
#
# @return [ Document ] The root document in the hierarchy.
def _root
object = self
object = object._parent while object._parent
object
end
# Is this document the root document of the hierarchy?
#
# @example Is the document the root?
# document._root?
#
# @return [ true | false ] If the document is the root.
def _root?
_parent ? false : true
end
end
end