-
Notifications
You must be signed in to change notification settings - Fork 20
/
collection_proxy.rb
378 lines (326 loc) · 12.1 KB
/
collection_proxy.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
378
# encoding: UTF-8
# frozen_string_literal: true
require "active_model"
require "active_support"
require "active_support/inflector"
require "active_support/core_ext/object"
require_relative "../pointer"
module Parse
# We use a delegate pattern to send notifications to the parent whenever the content of the internal array changes.
# The main requirement to using the proxy is to provide the list of initial items if any,
# the owner to be notified and the name of the attribute 'key'. With that, anytime the array
# will change, we will notify the delegate by sending :'key'_will_change! . The proxy can also
# be lazy when fetching the contents of the collection. Whenever the collection is accessed and
# the list is in a "not loaded" state (empty and loaded == false), we will send :'key_fetch!' to the delegate in order to
# populate the collection.
# A CollectionProxy is a special type of array wrapper that notifies a delegate
# object about changes to the array in order to perform dirty tracking. This is
# used for all Array properties in Parse::Objects. Subclasses of {CollectionProxy} are
# also available for supporting different association types such as an array of Parse pointers
# and Parse relations.
# @see PointerCollectionProxy
# @see RelationCollectionProxy
class CollectionProxy
include ::ActiveModel::Model
include ::ActiveModel::Dirty
include ::Enumerable
# @!attribute [r] delegate
# The object to be notified of changes to the collection.
# @return [Object]
# @!attribute [rw] loaded
# @return [Boolean] true/false whether the collection has been loaded.
# @!attribute [r] parse_class
# For some subclasses, this helps typecast the items in the collection.
# @return [String]
# @!attribute [r] key
# the name of the property key to use when sending notifications for _will_change! and _fetch!
# @return [String]
attr_accessor :collection, :delegate, :loaded, :parse_class
attr_reader :delegate, :key
# This is to use dirty tracking within the proxy
define_attribute_methods :collection
# Create a new CollectionProxy instance.
# @param collection [Array] the initial items to add to the collection.
# @param delegate [Object] the owner of the object that will receive the notifications.
# @param key [Symbol] the name of the key to use when sending notifications for _will_change! and _fetch!
# @param parse_class [String] (Optional) the Parse class type are the items of the collection.
# This is used to typecast the objects in the array to a particular Parse Object type.
# @see PointerCollectionProxy
# @see RelationCollectionProxy
def initialize(collection = nil, delegate: nil, key: nil, parse_class: nil)
@delegate = delegate
@key = key.to_sym if key.present?
@collection = collection.is_a?(Array) ? collection : []
@loaded = @collection.count > 0
@parse_class = parse_class
end
# true if the collection has been loaded
def loaded?
@loaded
end
# Forward a method call to the delegate.
# @param method [Symbol] the name of the method to forward
# @param params [Object] method parameters
# @return [Object] the return value from the forwarded method.
def forward(method, params = nil)
return unless @delegate && @delegate.respond_to?(method)
params.nil? ? @delegate.send(method) : @delegate.send(method, params)
end
# Reset the state of the collection.
def reset!
@loaded = false
clear
end
# @return [Boolean] true if two collection proxies have similar items.
def ==(other_list)
if other_list.is_a?(Array)
return @collection == other_list
elsif other_list.is_a?(Parse::CollectionProxy)
return @collection == other_list.instance_variable_get(:@collection)
end
end
# Reload and restore the collection to its original set of items.
def reload!
reset!
collection #force reload
end
# clear all items in the collection
def clear
@collection.clear
end
# @return [Array]
def to_a
collection.to_a
end;
alias_method :to_ary, :to_a
# Set the internal collection of items *without* dirty tracking or
# change notifications.
# @return [Array] the collection
def set_collection!(list)
@collection = list
end
# @!attribute [rw] collection
# The internal backing store of the collection containing the content. This value is lazily
# loaded for some subclasses.
# @note If you modify this directly, it is highly recommended that you
# call {CollectionProxy#notify_will_change!} to notify the dirty tracking system.
# @return [Array] contents of the collection.
def collection
if @collection.empty? && @loaded == false
@collection = forward(:"#{@key}_fetch!") || @collection || []
@loaded = true
end
@collection
end
def collection=(c)
notify_will_change!
@collection = c
end
# Add items to the collection
# @param items [Array] items to add
def add(*items)
notify_will_change! if items.count > 0
items.each do |item|
collection.push item
end
@collection
end;
alias_method :push, :add
# Add items to the collection if they don't already exist
# @param items [Array] items to uniquely add
# @return [Array] the collection.
def add_unique(*items)
return unless items.count > 0
notify_will_change!
@collection = collection | items.flatten
@collection
end;
alias_method :push_unique, :add_unique
# Set Union - Returns a new array by joining two arrays, excluding
# any duplicates and preserving the order from the original array.
# It compares elements using their hash and eql? methods for efficiency.
# See {https://docs.ruby-lang.org/en/2.0.0/Array.html#method-i-hash Array#|}
# @example
# [ "a", "b", "c" ] | [ "c", "d", "a" ] #=> [ "a", "b", "c", "d" ]
# @param items [Array] items to uniquely add
# @see #add_unique
# @return [Array] array with unique items
def |(items)
collection | [items].flatten
end
# Set Intersection - Returns a new array containing unique elements common
# to the two arrays. The order is preserved from the original array.
#
# It compares elements using their hash and eql? methods for efficiency.
# See {https://ruby-doc.org/core-2.4.1/Array.html#method-i-26 Array#&}
# @example
# [ 1, 1, 3, 5 ] & [ 3, 2, 1 ] #=> [ 1, 3 ]
# [ 'a', 'b', 'b', 'z' ] & [ 'a', 'b', 'c' ] #=> [ 'a', 'b' ]
# @param other_ary [Array]
# @return [Array] intersection array
def &(other_ary)
collection & [other_ary].flatten
end
# Alias {https://ruby-doc.org/core-2.4.1/Array.html#method-i-2D Array Difference}.
# Returns a new array that is a copy of the original array, removing any
# items that also appear in other_ary. The order is preserved from the
# original array.
# @example
# [ 1, 1, 2, 2, 3, 3, 4, 5 ] - [ 1, 2, 4 ] #=> [ 3, 3, 5 ]
# @param other_ary [Array]
# @return [Array] delta array
def -(other_ary)
collection - [other_ary].flatten
end
# Alias {https://ruby-doc.org/core-2.4.1/Array.html#method-i-2B Array Concatenation}.
# Returns a new array built by concatenating the two arrays together to
# produce a third array.
# @example
# [ 1, 2, 3 ] + [ 4, 5 ] #=> [ 1, 2, 3, 4, 5 ]
# @param other_ary [Array]
# @return [Array] concatenated array
def +(other_ary)
collection + [other_ary].flatten.to_a
end
# Alias {https://ruby-doc.org/core-2.4.1/Array.html#method-i-flatten Array flattening}.
# @return [Array] a flattened one-dimensional array
def flatten
collection.flatten
end
# Remove items from the collection
# @param items [Array] items to remove
def remove(*items)
notify_will_change! if items.count > 0
items.each do |item|
collection.delete item
end
@collection
end;
alias_method :delete, :remove
# Atomically adds all items from the array.
# This request is sent directly to the Parse backend.
# @param items [Array] items to uniquely add
# @see #add_unique!
def add!(*items)
return false unless @delegate.respond_to?(:op_add!)
@delegate.send :op_add!, @key, items.flatten
reset!
end
# Atomically adds all items from the array that are not already part of the collection.
# This request is sent directly to the Parse backend.
# @param items [Array] items to uniquely add
# @see #add!
def add_unique!(*items)
return false unless @delegate.respond_to?(:op_add_unique!)
@delegate.send :op_add_unique!, @key, items.flatten
reset!
end
# Atomically deletes all items from the array. This request is sent
# directly to the Parse backend.
# @param items [Array] items to remove
def remove!(*items)
return false unless @delegate.respond_to?(:op_remove!)
@delegate.send :op_remove!, @key, items.flatten
reset!
end
# Atomically deletes all items in the array, and marks the field as `undefined` directly
# with the Parse server. This request is sent directly to the Parse backend.
def destroy!
return false unless @delegate.respond_to?(:op_destroy!)
@delegate.send :op_destroy!, @key
collection_will_change!
@collection.clear
reset!
end
# Locally restores previous attributes (not from the persistent store)
def rollback!
restore_attributes
end
# clears all dirty tracked information.
def clear_changes!
clear_changes_information
end
# mark that collection changes where applied, which clears dirty tracking.
def changes_applied!
changes_applied
end
# @param args [Hash] arguments to pass to Array#first.
# @return [Object] the first item in the collection
def first(*args)
collection.first(*args)
end
# @return [Object] the second item in the collection
def second
collection.second
end
# @param args [Hash] arguments to pass to Array#last.
# @return [Object] the last item in the collection
def last(*args)
collection.last(*args)
end
# @return [Integer] number of items in the collection.
def count
collection.count
end
# @return [Hash] a JSON representation
def as_json(opts = nil)
collection.as_json(opts)
end
# true if the collection is empty.
def empty?
collection.empty?
end
# Append items to the collection
def <<(*list)
if list.count > 0
notify_will_change!
list.flatten.each { |e| collection.push(e) }
end
end
# Notifies the delegate that the collection changed.
def notify_will_change!
collection_will_change!
forward "#{@key}_will_change!"
end
# Alias for Array#each
def each
return collection.enum_for(:each) unless block_given?
collection.each &Proc.new
end
# Alias for Array#map
def map
return collection.enum_for(:map) unless block_given?
collection.map &Proc.new
end
# Alias for Array#select
def select
return collection.enum_for(:select) unless block_given?
collection.select &Proc.new
end
# Alias for Array#uniq
def uniq
return collection.uniq(&Proc.new) if block_given?
return collection.uniq
end
# Alias for Array#uniq!
def uniq!
notify_will_change!
return collection.uniq!(&Proc.new) if block_given?
return collection.uniq!
end
# @!visibility private
def inspect
"#<#{self.class} changed?=#{changed?} @collection=#{@collection.inspect} >"
end
# Alias to `to_a.parse_objects` from Array#parse_objects
# @return [Array<Parse::Object>] an array of Parse Object subclasses representing this collection.
def parse_objects
collection.to_a.parse_objects
end
# Alias to `to_a.parse_pointers` from Array#parse_pointers
# @return [Array<Parse::Pointer>] an array of pointers representing this collection.
def parse_pointers
collection.to_a.parse_pointers
end
end
end