formed 1.0.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 +7 -0
- data/README.md +146 -0
- data/Rakefile +12 -0
- data/lib/active_form.rb +12 -0
- data/lib/formed/acts_like_model.rb +27 -0
- data/lib/formed/association_relation.rb +22 -0
- data/lib/formed/associations/association.rb +193 -0
- data/lib/formed/associations/builder/association.rb +116 -0
- data/lib/formed/associations/builder/collection_association.rb +71 -0
- data/lib/formed/associations/builder/has_many.rb +24 -0
- data/lib/formed/associations/builder/has_one.rb +44 -0
- data/lib/formed/associations/builder/singular_association.rb +46 -0
- data/lib/formed/associations/builder.rb +13 -0
- data/lib/formed/associations/collection_association.rb +296 -0
- data/lib/formed/associations/collection_proxy.rb +519 -0
- data/lib/formed/associations/foreign_association.rb +37 -0
- data/lib/formed/associations/has_many_association.rb +63 -0
- data/lib/formed/associations/has_one_association.rb +27 -0
- data/lib/formed/associations/singular_association.rb +66 -0
- data/lib/formed/associations.rb +62 -0
- data/lib/formed/attributes.rb +42 -0
- data/lib/formed/base.rb +183 -0
- data/lib/formed/core.rb +73 -0
- data/lib/formed/from_model.rb +41 -0
- data/lib/formed/from_params.rb +33 -0
- data/lib/formed/inheritance.rb +179 -0
- data/lib/formed/nested_attributes.rb +287 -0
- data/lib/formed/reflection.rb +781 -0
- data/lib/formed/relation/delegation.rb +147 -0
- data/lib/formed/relation.rb +113 -0
- data/lib/formed/version.rb +3 -0
- data/lib/generators/active_form/form_generator.rb +72 -0
- data/lib/generators/active_form/templates/form.rb.tt +8 -0
- data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
- data/lib/generators/active_form/templates/module.rb.tt +4 -0
- metadata +203 -0
@@ -0,0 +1,519 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
class CollectionProxy < Relation
|
6
|
+
def initialize(klass, association, **) # :nodoc:
|
7
|
+
@association = association
|
8
|
+
super klass
|
9
|
+
|
10
|
+
extensions = association.extensions
|
11
|
+
extend(*extensions) if extensions.any?
|
12
|
+
end
|
13
|
+
|
14
|
+
def target
|
15
|
+
@association.target
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_target
|
19
|
+
@association.load_target
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns +true+ if the association has been loaded, otherwise +false+.
|
23
|
+
#
|
24
|
+
# person.pets.loaded? # => false
|
25
|
+
# person.pets.records
|
26
|
+
# person.pets.loaded? # => true
|
27
|
+
def loaded?
|
28
|
+
@association.loaded?
|
29
|
+
end
|
30
|
+
alias loaded loaded?
|
31
|
+
|
32
|
+
def last(limit = nil)
|
33
|
+
if limit
|
34
|
+
target.last(limit)
|
35
|
+
else
|
36
|
+
target.last
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Gives a record (or N records if a parameter is supplied) from the collection
|
41
|
+
# using the same rules as <tt>ActiveRecord::Base.take</tt>.
|
42
|
+
#
|
43
|
+
# class Person < ActiveRecord::Base
|
44
|
+
# has_many :pets
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# person.pets
|
48
|
+
# # => [
|
49
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
50
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>,
|
51
|
+
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
|
52
|
+
# # ]
|
53
|
+
#
|
54
|
+
# person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
|
55
|
+
#
|
56
|
+
# person.pets.take(2)
|
57
|
+
# # => [
|
58
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
59
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>
|
60
|
+
# # ]
|
61
|
+
#
|
62
|
+
# another_person_without.pets # => []
|
63
|
+
# another_person_without.pets.take # => nil
|
64
|
+
# another_person_without.pets.take(2) # => []
|
65
|
+
def take(limit = nil)
|
66
|
+
target.take(limit)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a new object of the collection type that has been instantiated
|
70
|
+
# with +attributes+ and linked to this object, but have not yet been saved.
|
71
|
+
# You can pass an array of attributes hashes, this will return an array
|
72
|
+
# with the new objects.
|
73
|
+
#
|
74
|
+
# class Person
|
75
|
+
# has_many :pets
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# person.pets.build
|
79
|
+
# # => #<Pet id: nil, name: nil, person_id: 1>
|
80
|
+
#
|
81
|
+
# person.pets.build(name: 'Fancy-Fancy')
|
82
|
+
# # => #<Pet id: nil, name: "Fancy-Fancy", person_id: 1>
|
83
|
+
#
|
84
|
+
# person.pets.build([{name: 'Spook'}, {name: 'Choo-Choo'}, {name: 'Brain'}])
|
85
|
+
# # => [
|
86
|
+
# # #<Pet id: nil, name: "Spook", person_id: 1>,
|
87
|
+
# # #<Pet id: nil, name: "Choo-Choo", person_id: 1>,
|
88
|
+
# # #<Pet id: nil, name: "Brain", person_id: 1>
|
89
|
+
# # ]
|
90
|
+
#
|
91
|
+
# person.pets.size # => 5 # size of the collection
|
92
|
+
# person.pets.count # => 0 # count from database
|
93
|
+
def build(attributes = {}, &block)
|
94
|
+
@association.build(attributes, &block)
|
95
|
+
end
|
96
|
+
alias new build
|
97
|
+
|
98
|
+
def with_context(context)
|
99
|
+
each { |record| record.with_context(context) }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Replaces this collection with +other_array+. This will perform a diff
|
103
|
+
# and delete/add only records that have changed.
|
104
|
+
#
|
105
|
+
# class Person < ActiveRecord::Base
|
106
|
+
# has_many :pets
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# person.pets
|
110
|
+
# # => [#<Pet id: 1, name: "Gorby", group: "cats", person_id: 1>]
|
111
|
+
#
|
112
|
+
# other_pets = [Pet.new(name: 'Puff', group: 'celebrities')]
|
113
|
+
#
|
114
|
+
# person.pets.replace(other_pets)
|
115
|
+
#
|
116
|
+
# person.pets
|
117
|
+
# # => [#<Pet id: 2, name: "Puff", group: "celebrities", person_id: 1>]
|
118
|
+
#
|
119
|
+
# If the supplied array has an incorrect association type, it raises
|
120
|
+
# an <tt>ActiveRecord::AssociationTypeMismatch</tt> error:
|
121
|
+
#
|
122
|
+
# person.pets.replace(["doo", "ggie", "gaga"])
|
123
|
+
# # => ActiveRecord::AssociationTypeMismatch: Pet expected, got String
|
124
|
+
def replace(other_array)
|
125
|
+
@association.replace(other_array)
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# :method: count
|
130
|
+
#
|
131
|
+
# :call-seq:
|
132
|
+
# count(column_name = nil, &block)
|
133
|
+
#
|
134
|
+
# Count all records.
|
135
|
+
#
|
136
|
+
# class Person < ActiveRecord::Base
|
137
|
+
# has_many :pets
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# # This will perform the count using SQL.
|
141
|
+
# person.pets.count # => 3
|
142
|
+
# person.pets
|
143
|
+
# # => [
|
144
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
145
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>,
|
146
|
+
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
|
147
|
+
# # ]
|
148
|
+
#
|
149
|
+
# Passing a block will select all of a person's pets in SQL and then
|
150
|
+
# perform the count using Ruby.
|
151
|
+
#
|
152
|
+
# person.pets.count { |pet| pet.name.include?('-') } # => 2
|
153
|
+
|
154
|
+
# Returns the size of the collection. If the collection hasn't been loaded,
|
155
|
+
# it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
|
156
|
+
#
|
157
|
+
# If the collection has been already loaded +size+ and +length+ are
|
158
|
+
# equivalent. If not and you are going to need the records anyway
|
159
|
+
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
160
|
+
#
|
161
|
+
# class Person < ActiveRecord::Base
|
162
|
+
# has_many :pets
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# person.pets.size # => 3
|
166
|
+
# # executes something like SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" = 1
|
167
|
+
#
|
168
|
+
# person.pets # This will execute a SELECT * FROM query
|
169
|
+
# # => [
|
170
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
171
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>,
|
172
|
+
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
|
173
|
+
# # ]
|
174
|
+
#
|
175
|
+
# person.pets.size # => 3
|
176
|
+
# # Because the collection is already loaded, this will behave like
|
177
|
+
# # collection.size and no SQL count query is executed.
|
178
|
+
def size
|
179
|
+
@association.size
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# :method: length
|
184
|
+
#
|
185
|
+
# :call-seq:
|
186
|
+
# length()
|
187
|
+
#
|
188
|
+
# Returns the size of the collection calling +size+ on the target.
|
189
|
+
# If the collection has been already loaded, +length+ and +size+ are
|
190
|
+
# equivalent. If not and you are going to need the records anyway this
|
191
|
+
# method will take one less query. Otherwise +size+ is more efficient.
|
192
|
+
#
|
193
|
+
# class Person < ActiveRecord::Base
|
194
|
+
# has_many :pets
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# person.pets.length # => 3
|
198
|
+
# # executes something like SELECT "pets".* FROM "pets" WHERE "pets"."person_id" = 1
|
199
|
+
#
|
200
|
+
# # Because the collection is loaded, you can
|
201
|
+
# # call the collection with no additional queries:
|
202
|
+
# person.pets
|
203
|
+
# # => [
|
204
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
205
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>,
|
206
|
+
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
|
207
|
+
# # ]
|
208
|
+
|
209
|
+
# Returns +true+ if the collection is empty. If the collection has been
|
210
|
+
# loaded it is equivalent
|
211
|
+
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
|
212
|
+
# it is equivalent to <tt>!collection.exists?</tt>. If the collection has
|
213
|
+
# not already been loaded and you are going to fetch the records anyway it
|
214
|
+
# is better to check <tt>collection.load.empty?</tt>.
|
215
|
+
#
|
216
|
+
# class Person < ActiveRecord::Base
|
217
|
+
# has_many :pets
|
218
|
+
# end
|
219
|
+
#
|
220
|
+
# person.pets.count # => 1
|
221
|
+
# person.pets.empty? # => false
|
222
|
+
#
|
223
|
+
# person.pets.delete_all
|
224
|
+
#
|
225
|
+
# person.pets.count # => 0
|
226
|
+
# person.pets.empty? # => true
|
227
|
+
def empty?
|
228
|
+
@association.empty?
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
# :method: any?
|
233
|
+
#
|
234
|
+
# :call-seq:
|
235
|
+
# any?()
|
236
|
+
#
|
237
|
+
# Returns +true+ if the collection is not empty.
|
238
|
+
#
|
239
|
+
# class Person < ActiveRecord::Base
|
240
|
+
# has_many :pets
|
241
|
+
# end
|
242
|
+
#
|
243
|
+
# person.pets.count # => 0
|
244
|
+
# person.pets.any? # => false
|
245
|
+
#
|
246
|
+
# person.pets << Pet.new(name: 'Snoop')
|
247
|
+
# person.pets.count # => 1
|
248
|
+
# person.pets.any? # => true
|
249
|
+
#
|
250
|
+
# Calling it without a block when the collection is not yet
|
251
|
+
# loaded is equivalent to <tt>collection.exists?</tt>.
|
252
|
+
# If you're going to load the collection anyway, it is better
|
253
|
+
# to call <tt>collection.load.any?</tt> to avoid an extra query.
|
254
|
+
#
|
255
|
+
# You can also pass a +block+ to define criteria. The behavior
|
256
|
+
# is the same, it returns true if the collection based on the
|
257
|
+
# criteria is not empty.
|
258
|
+
#
|
259
|
+
# person.pets
|
260
|
+
# # => [#<Pet name: "Snoop", group: "dogs">]
|
261
|
+
#
|
262
|
+
# person.pets.any? do |pet|
|
263
|
+
# pet.group == 'cats'
|
264
|
+
# end
|
265
|
+
# # => false
|
266
|
+
#
|
267
|
+
# person.pets.any? do |pet|
|
268
|
+
# pet.group == 'dogs'
|
269
|
+
# end
|
270
|
+
# # => true
|
271
|
+
|
272
|
+
##
|
273
|
+
# :method: many?
|
274
|
+
#
|
275
|
+
# :call-seq:
|
276
|
+
# many?()
|
277
|
+
#
|
278
|
+
# Returns true if the collection has more than one record.
|
279
|
+
# Equivalent to <tt>collection.size > 1</tt>.
|
280
|
+
#
|
281
|
+
# class Person < ActiveRecord::Base
|
282
|
+
# has_many :pets
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# person.pets.count # => 1
|
286
|
+
# person.pets.many? # => false
|
287
|
+
#
|
288
|
+
# person.pets << Pet.new(name: 'Snoopy')
|
289
|
+
# person.pets.count # => 2
|
290
|
+
# person.pets.many? # => true
|
291
|
+
#
|
292
|
+
# You can also pass a +block+ to define criteria. The
|
293
|
+
# behavior is the same, it returns true if the collection
|
294
|
+
# based on the criteria has more than one record.
|
295
|
+
#
|
296
|
+
# person.pets
|
297
|
+
# # => [
|
298
|
+
# # #<Pet name: "Gorby", group: "cats">,
|
299
|
+
# # #<Pet name: "Puff", group: "cats">,
|
300
|
+
# # #<Pet name: "Snoop", group: "dogs">
|
301
|
+
# # ]
|
302
|
+
#
|
303
|
+
# person.pets.many? do |pet|
|
304
|
+
# pet.group == 'dogs'
|
305
|
+
# end
|
306
|
+
# # => false
|
307
|
+
#
|
308
|
+
# person.pets.many? do |pet|
|
309
|
+
# pet.group == 'cats'
|
310
|
+
# end
|
311
|
+
# # => true
|
312
|
+
|
313
|
+
# Returns +true+ if the given +record+ is present in the collection.
|
314
|
+
#
|
315
|
+
# class Person < ActiveRecord::Base
|
316
|
+
# has_many :pets
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# person.pets # => [#<Pet id: 20, name: "Snoop">]
|
320
|
+
#
|
321
|
+
# person.pets.include?(Pet.find(20)) # => true
|
322
|
+
# person.pets.include?(Pet.find(21)) # => false
|
323
|
+
def include?(record)
|
324
|
+
!!@association.include?(record)
|
325
|
+
end
|
326
|
+
|
327
|
+
def proxy_association # :nodoc:
|
328
|
+
@association
|
329
|
+
end
|
330
|
+
|
331
|
+
# Returns a <tt>Relation</tt> object for the records in this association
|
332
|
+
def scope
|
333
|
+
@scope ||= @association.scope
|
334
|
+
end
|
335
|
+
|
336
|
+
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
|
337
|
+
# contain the same number of elements and if each element is equal
|
338
|
+
# to the corresponding element in the +other+ array, otherwise returns
|
339
|
+
# +false+.
|
340
|
+
#
|
341
|
+
# class Person < ActiveRecord::Base
|
342
|
+
# has_many :pets
|
343
|
+
# end
|
344
|
+
#
|
345
|
+
# person.pets
|
346
|
+
# # => [
|
347
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
348
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>
|
349
|
+
# # ]
|
350
|
+
#
|
351
|
+
# other = person.pets.to_ary
|
352
|
+
#
|
353
|
+
# person.pets == other
|
354
|
+
# # => true
|
355
|
+
#
|
356
|
+
# other = [Pet.new(id: 1), Pet.new(id: 2)]
|
357
|
+
#
|
358
|
+
# person.pets == other
|
359
|
+
# # => false
|
360
|
+
def ==(other)
|
361
|
+
load_target == other
|
362
|
+
end
|
363
|
+
|
364
|
+
##
|
365
|
+
# :method: to_ary
|
366
|
+
#
|
367
|
+
# :call-seq:
|
368
|
+
# to_ary()
|
369
|
+
#
|
370
|
+
# Returns a new array of objects from the collection. If the collection
|
371
|
+
# hasn't been loaded, it fetches the records from the database.
|
372
|
+
#
|
373
|
+
# class Person < ActiveRecord::Base
|
374
|
+
# has_many :pets
|
375
|
+
# end
|
376
|
+
#
|
377
|
+
# person.pets
|
378
|
+
# # => [
|
379
|
+
# # #<Pet id: 4, name: "Benny", person_id: 1>,
|
380
|
+
# # #<Pet id: 5, name: "Brain", person_id: 1>,
|
381
|
+
# # #<Pet id: 6, name: "Boss", person_id: 1>
|
382
|
+
# # ]
|
383
|
+
#
|
384
|
+
# other_pets = person.pets.to_ary
|
385
|
+
# # => [
|
386
|
+
# # #<Pet id: 4, name: "Benny", person_id: 1>,
|
387
|
+
# # #<Pet id: 5, name: "Brain", person_id: 1>,
|
388
|
+
# # #<Pet id: 6, name: "Boss", person_id: 1>
|
389
|
+
# # ]
|
390
|
+
#
|
391
|
+
# other_pets.replace([Pet.new(name: 'BooGoo')])
|
392
|
+
#
|
393
|
+
# other_pets
|
394
|
+
# # => [#<Pet id: nil, name: "BooGoo", person_id: 1>]
|
395
|
+
#
|
396
|
+
# person.pets
|
397
|
+
# # This is not affected by replace
|
398
|
+
# # => [
|
399
|
+
# # #<Pet id: 4, name: "Benny", person_id: 1>,
|
400
|
+
# # #<Pet id: 5, name: "Brain", person_id: 1>,
|
401
|
+
# # #<Pet id: 6, name: "Boss", person_id: 1>
|
402
|
+
# # ]
|
403
|
+
|
404
|
+
def records # :nodoc:
|
405
|
+
load_target
|
406
|
+
end
|
407
|
+
|
408
|
+
# Adds one or more +records+ to the collection by setting their foreign keys
|
409
|
+
# to the association's primary key. Since <tt><<</tt> flattens its argument list and
|
410
|
+
# inserts each record, +push+ and +concat+ behave identically. Returns +self+
|
411
|
+
# so several appends may be chained together.
|
412
|
+
#
|
413
|
+
# class Person < ActiveRecord::Base
|
414
|
+
# has_many :pets
|
415
|
+
# end
|
416
|
+
#
|
417
|
+
# person.pets.size # => 0
|
418
|
+
# person.pets << Pet.new(name: 'Fancy-Fancy')
|
419
|
+
# person.pets << [Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo')]
|
420
|
+
# person.pets.size # => 3
|
421
|
+
#
|
422
|
+
# person.id # => 1
|
423
|
+
# person.pets
|
424
|
+
# # => [
|
425
|
+
# # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
|
426
|
+
# # #<Pet id: 2, name: "Spook", person_id: 1>,
|
427
|
+
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
|
428
|
+
# # ]
|
429
|
+
def <<(*records)
|
430
|
+
proxy_association.concat(records) && self
|
431
|
+
end
|
432
|
+
alias push <<
|
433
|
+
alias append <<
|
434
|
+
alias concat <<
|
435
|
+
|
436
|
+
def prepend(*_args) # :nodoc:
|
437
|
+
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
|
438
|
+
end
|
439
|
+
|
440
|
+
# Equivalent to +delete_all+. The difference is that returns +self+, instead
|
441
|
+
# of an array with the deleted objects, so methods can be chained. See
|
442
|
+
# +delete_all+ for more information.
|
443
|
+
# Note that because +delete_all+ removes records by directly
|
444
|
+
# running an SQL query into the database, the +updated_at+ column of
|
445
|
+
# the object is not changed.
|
446
|
+
def clear
|
447
|
+
delete_all
|
448
|
+
self
|
449
|
+
end
|
450
|
+
|
451
|
+
# Reloads the collection from the database. Returns +self+.
|
452
|
+
#
|
453
|
+
# class Person < ActiveRecord::Base
|
454
|
+
# has_many :pets
|
455
|
+
# end
|
456
|
+
#
|
457
|
+
# person.pets # fetches pets from the database
|
458
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
459
|
+
#
|
460
|
+
# person.pets # uses the pets cache
|
461
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
462
|
+
#
|
463
|
+
# person.pets.reload # fetches pets from the database
|
464
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
465
|
+
def reload
|
466
|
+
proxy_association.reload(true)
|
467
|
+
reset_scope
|
468
|
+
end
|
469
|
+
|
470
|
+
# Unloads the association. Returns +self+.
|
471
|
+
#
|
472
|
+
# class Person < ActiveRecord::Base
|
473
|
+
# has_many :pets
|
474
|
+
# end
|
475
|
+
#
|
476
|
+
# person.pets # fetches pets from the database
|
477
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
478
|
+
#
|
479
|
+
# person.pets # uses the pets cache
|
480
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
481
|
+
#
|
482
|
+
# person.pets.reset # clears the pets cache
|
483
|
+
#
|
484
|
+
# person.pets # fetches pets from the database
|
485
|
+
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
|
486
|
+
def reset
|
487
|
+
end
|
488
|
+
|
489
|
+
def reset_scope # :nodoc:
|
490
|
+
self
|
491
|
+
end
|
492
|
+
|
493
|
+
def inspect # :nodoc:
|
494
|
+
load_target if find_from_target?
|
495
|
+
super
|
496
|
+
end
|
497
|
+
|
498
|
+
private
|
499
|
+
|
500
|
+
def find_nth_with_limit(index, limit)
|
501
|
+
load_target if find_from_target?
|
502
|
+
super
|
503
|
+
end
|
504
|
+
|
505
|
+
def find_nth_from_last(index)
|
506
|
+
load_target if find_from_target?
|
507
|
+
super
|
508
|
+
end
|
509
|
+
|
510
|
+
def find_from_target?
|
511
|
+
@association.find_from_target?
|
512
|
+
end
|
513
|
+
|
514
|
+
def exec_queries
|
515
|
+
load_target
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
module ForeignAssociation # :nodoc:
|
6
|
+
def foreign_key_present?
|
7
|
+
if reflection.klass.primary_key
|
8
|
+
owner.attribute_present?(reflection.active_record_primary_key)
|
9
|
+
else
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def nullified_owner_attributes
|
15
|
+
{}.tap do |attrs|
|
16
|
+
attrs[reflection.foreign_key] = nil
|
17
|
+
attrs[reflection.type] = nil if reflection.type.present?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Sets the owner attributes on the given record
|
24
|
+
def set_owner_attributes(record)
|
25
|
+
return if options[:through]
|
26
|
+
|
27
|
+
return
|
28
|
+
key = owner.send(:_read_attribute, reflection.join_foreign_key)
|
29
|
+
record._write_attribute(reflection.join_primary_key, key)
|
30
|
+
|
31
|
+
return unless reflection.type
|
32
|
+
|
33
|
+
record._write_attribute(reflection.type, owner.class.polymorphic_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
class HasManyAssociation < CollectionAssociation # :nodoc:
|
6
|
+
include ForeignAssociation
|
7
|
+
|
8
|
+
def insert_record(record, validate = true, raise = false)
|
9
|
+
set_owner_attributes(record)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Returns the number of records in this collection.
|
16
|
+
#
|
17
|
+
# If the association has a counter cache it gets that value. Otherwise
|
18
|
+
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
19
|
+
# there's one. Some configuration options like :group make it impossible
|
20
|
+
# to do an SQL count, in those cases the array count will be used.
|
21
|
+
#
|
22
|
+
# That does not depend on whether the collection has already been loaded
|
23
|
+
# or not. The +size+ method is the one that takes the loaded flag into
|
24
|
+
# account and delegates to +count_records+ if needed.
|
25
|
+
#
|
26
|
+
# If the collection is empty the target is set to an empty array and
|
27
|
+
# the loaded flag is set to true as well.
|
28
|
+
def count_records
|
29
|
+
count = if reflection.has_cached_counter?
|
30
|
+
owner.read_attribute(reflection.counter_cache_column).to_i
|
31
|
+
else
|
32
|
+
scope.count(:all)
|
33
|
+
end
|
34
|
+
|
35
|
+
# If there's nothing in the database, @target should only contain new
|
36
|
+
# records or be an empty array. This is a documented side-effect of
|
37
|
+
# the method that may avoid an extra SELECT.
|
38
|
+
if count.zero?
|
39
|
+
target.select!(&:new_record?)
|
40
|
+
loaded!
|
41
|
+
end
|
42
|
+
|
43
|
+
[10, count].compact.min
|
44
|
+
end
|
45
|
+
|
46
|
+
def _create_record(attributes, *)
|
47
|
+
if attributes.is_a?(Array)
|
48
|
+
super
|
49
|
+
else
|
50
|
+
update_counter_if_success(super, 1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def difference(a, b)
|
55
|
+
a - b
|
56
|
+
end
|
57
|
+
|
58
|
+
def intersection(a, b)
|
59
|
+
a & b
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
class HasOneAssociation < SingularAssociation # :nodoc:
|
6
|
+
include ForeignAssociation
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def replace(record, _save = true)
|
11
|
+
return target unless load_target || record
|
12
|
+
|
13
|
+
target
|
14
|
+
|
15
|
+
self.target = record
|
16
|
+
end
|
17
|
+
|
18
|
+
def set_new_record(record)
|
19
|
+
replace(record, false)
|
20
|
+
end
|
21
|
+
|
22
|
+
def nullify_owner_attributes(_record)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Formed
|
4
|
+
module Associations
|
5
|
+
class SingularAssociation < Association # :nodoc:
|
6
|
+
# Implements the reader method, e.g. foo.bar for Foo.has_one :bar
|
7
|
+
def reader
|
8
|
+
ensure_klass_exists!
|
9
|
+
|
10
|
+
reload if !loaded? || stale_target?
|
11
|
+
|
12
|
+
target
|
13
|
+
end
|
14
|
+
|
15
|
+
# Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
|
16
|
+
def writer(record)
|
17
|
+
record = build_record(record) unless record.class < Formed::Base
|
18
|
+
replace(record)
|
19
|
+
end
|
20
|
+
|
21
|
+
def build(attributes = nil, &block)
|
22
|
+
record = build_record(attributes, &block)
|
23
|
+
set_new_record(record)
|
24
|
+
record
|
25
|
+
end
|
26
|
+
|
27
|
+
# Implements the reload reader method, e.g. foo.reload_bar for
|
28
|
+
# Foo.has_one :bar
|
29
|
+
def force_reload_reader
|
30
|
+
reload(true)
|
31
|
+
target
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def scope_for_create
|
37
|
+
super.except!(klass.primary_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_target
|
41
|
+
if disable_joins
|
42
|
+
scope.first
|
43
|
+
else
|
44
|
+
super.first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def replace(record)
|
49
|
+
raise NotImplementedError, "Subclasses must implement a replace(record) method"
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_new_record(record)
|
53
|
+
replace(record)
|
54
|
+
end
|
55
|
+
|
56
|
+
def _create_record(attributes, raise_error = false, &block)
|
57
|
+
record = build_record(attributes, &block)
|
58
|
+
saved = record.save
|
59
|
+
set_new_record(record)
|
60
|
+
raise RecordInvalid, record if !saved && raise_error
|
61
|
+
|
62
|
+
record
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|