formed 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +146 -0
  3. data/Rakefile +12 -0
  4. data/lib/active_form.rb +12 -0
  5. data/lib/formed/acts_like_model.rb +27 -0
  6. data/lib/formed/association_relation.rb +22 -0
  7. data/lib/formed/associations/association.rb +193 -0
  8. data/lib/formed/associations/builder/association.rb +116 -0
  9. data/lib/formed/associations/builder/collection_association.rb +71 -0
  10. data/lib/formed/associations/builder/has_many.rb +24 -0
  11. data/lib/formed/associations/builder/has_one.rb +44 -0
  12. data/lib/formed/associations/builder/singular_association.rb +46 -0
  13. data/lib/formed/associations/builder.rb +13 -0
  14. data/lib/formed/associations/collection_association.rb +296 -0
  15. data/lib/formed/associations/collection_proxy.rb +519 -0
  16. data/lib/formed/associations/foreign_association.rb +37 -0
  17. data/lib/formed/associations/has_many_association.rb +63 -0
  18. data/lib/formed/associations/has_one_association.rb +27 -0
  19. data/lib/formed/associations/singular_association.rb +66 -0
  20. data/lib/formed/associations.rb +62 -0
  21. data/lib/formed/attributes.rb +42 -0
  22. data/lib/formed/base.rb +183 -0
  23. data/lib/formed/core.rb +73 -0
  24. data/lib/formed/from_model.rb +41 -0
  25. data/lib/formed/from_params.rb +33 -0
  26. data/lib/formed/inheritance.rb +179 -0
  27. data/lib/formed/nested_attributes.rb +287 -0
  28. data/lib/formed/reflection.rb +781 -0
  29. data/lib/formed/relation/delegation.rb +147 -0
  30. data/lib/formed/relation.rb +113 -0
  31. data/lib/formed/version.rb +3 -0
  32. data/lib/generators/active_form/form_generator.rb +72 -0
  33. data/lib/generators/active_form/templates/form.rb.tt +8 -0
  34. data/lib/generators/active_form/templates/form_spec.rb.tt +5 -0
  35. data/lib/generators/active_form/templates/module.rb.tt +4 -0
  36. 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