deferring 0.0.2 → 0.0.3
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 +207 -41
- data/deferring.gemspec +1 -1
- data/gemfiles/rails_30.gemfile.lock +1 -1
- data/gemfiles/rails_32.gemfile.lock +1 -1
- data/gemfiles/rails_40.gemfile.lock +1 -1
- data/gemfiles/rails_41.gemfile.lock +1 -1
- data/lib/deferring.rb +52 -30
- data/lib/deferring/deferred_association.rb +66 -44
- data/lib/deferring/deferred_callback_listener.rb +11 -0
- data/lib/deferring/version.rb +1 -1
- data/spec/lib/deferring_spec.rb +108 -19
- data/spec/support/models.rb +39 -16
- metadata +25 -38
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 533c02589a0e9c2ae8d48d47d3746ae7234633e7
|
4
|
+
data.tar.gz: cd023ba8e4b6f3cb9075a667503b26c1c8a60354
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 90d31875102fdd4120714e484f7e30770f2324663fabc5dde2925787c63264fc41812458d4f0605c3b9b1e413654ab8613f52e5aaf01255024564c4c5e7083df
|
7
|
+
data.tar.gz: 2f4042d509e4b1f497e6b04db21bbf35fee8cddb28254e7ee24e7bf9425adf0be142c6dbeceed1c47a7eaf8a0a2ce8ad424635993d2e61f82c0d7040c34bf355
|
data/README.md
CHANGED
@@ -12,8 +12,6 @@ It is important to note that Deferring does not touch the original `has_many`
|
|
12
12
|
and `has_and_belongs_to_many` associations. You can use them, without worrying
|
13
13
|
about any changed behaviour or side-effects from using Deferring.
|
14
14
|
|
15
|
-
**NOTE: This is currently work in progress.**
|
16
|
-
|
17
15
|
## Why use it?
|
18
16
|
|
19
17
|
Let's take a look at the following example:
|
@@ -119,18 +117,156 @@ Or install it yourself as:
|
|
119
117
|
|
120
118
|
### How do I use it?
|
121
119
|
|
122
|
-
Deferring adds a couple of methods to your ActiveRecord
|
120
|
+
Deferring adds a couple of methods to your ActiveRecord classes. These are:
|
123
121
|
|
124
122
|
- `deferred_has_and_belongs_to_many`
|
125
123
|
- `deferred_accepts_nested_attributes_for`
|
126
124
|
- `deferred_has_many`
|
127
125
|
|
128
|
-
These methods wrap the existing methods. For instance,
|
129
|
-
call `
|
126
|
+
These methods wrap the existing methods. For instance,
|
127
|
+
`deferred_has_and_belongs_to_many` will call `has_and_belongs_to_many` in order
|
128
|
+
to set up the association.
|
129
|
+
|
130
|
+
In order to create a deferred association, you can just replace the regular
|
131
|
+
method by one provided by Deferring.
|
132
|
+
|
133
|
+
Simple!
|
134
|
+
|
135
|
+
Next to that, Deferring adds the following functionality:
|
136
|
+
* new callbacks that are triggered when adding/removing a record to the
|
137
|
+
deferred association before saving the parent, and
|
138
|
+
* new methods on the deferred association to retrieve the records that are to be
|
139
|
+
linked to/unlinked from the parent.
|
140
|
+
|
141
|
+
#### Callbacks
|
142
|
+
|
143
|
+
##### Rails' callbacks
|
144
|
+
|
145
|
+
You can use the regular Rails callbacks on deferred associations. However, these
|
146
|
+
callbacks are triggered at a different point in time.
|
147
|
+
|
148
|
+
An example to illustrate:
|
149
|
+
|
150
|
+
``` ruby
|
151
|
+
class Person < ActiveRecord::Base
|
152
|
+
has_and_belongs_to_many :teams, before_add: :before_adding
|
153
|
+
deferred_has_and_belongs_to_many :pets, before_add: :before_adding
|
154
|
+
|
155
|
+
def audit_log
|
156
|
+
@log = []
|
157
|
+
end
|
158
|
+
|
159
|
+
def before_adding(record)
|
160
|
+
audit_log << "Before adding #{record.class} with id #{record.id}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
This sets up a Person model that has a regular HABTM association with teams and
|
166
|
+
that has a deferred HABTM association with pets. Each time a team or pet is
|
167
|
+
added to the database a log statement is written to the audit log (using the
|
168
|
+
`before_adding` callback method).
|
169
|
+
|
170
|
+
The regular HABTM association behaves likes this:
|
171
|
+
|
172
|
+
``` ruby
|
173
|
+
person = Person.first
|
174
|
+
person.teams << Team.find(1)
|
175
|
+
person.audit_log # => ['Before adding Team 1']
|
176
|
+
```
|
177
|
+
|
178
|
+
As records of deferred associations are saved to the database after saving the
|
179
|
+
parent the behavior is a bit different:
|
180
|
+
|
181
|
+
``` ruby
|
182
|
+
person = Person.first
|
183
|
+
person.pets << Pet.find(1)
|
184
|
+
person.audit_log # => []
|
185
|
+
|
186
|
+
person.save
|
187
|
+
person.audit_log # => ['Before adding Pet 1']
|
188
|
+
```
|
189
|
+
|
190
|
+
##### New link and unlink callbacks
|
191
|
+
|
192
|
+
As said, the regular `:before_add`, etc. callbacks still work, but they are only
|
193
|
+
triggered after the parent object has been saved. You can use the following
|
194
|
+
callbacks when you want a method to be executed when adding/deleting a record to
|
195
|
+
the deferred association *before saving* the parent:
|
196
|
+
* `:before_link`
|
197
|
+
* `:after_link`
|
198
|
+
* `:before_unlink`
|
199
|
+
* `:after_unlink`
|
200
|
+
|
201
|
+
Another example to illustrate:
|
202
|
+
|
203
|
+
``` ruby
|
204
|
+
class Person < ActiveRecord::Base
|
205
|
+
deferred_has_and_belongs_to_many :pets, before_link: :link_pet
|
206
|
+
|
207
|
+
def audit_log
|
208
|
+
@log = []
|
209
|
+
end
|
210
|
+
|
211
|
+
def link_pet(pet)
|
212
|
+
audit_log << "Before linking #{pet.class} with id #{pet.id}"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
This sets up a Person model that has a deferred HABTM association to pets. Each
|
218
|
+
time a pet is linked to the Person a log statement is written to the audit log
|
219
|
+
(using the `link_pet` callback function).
|
220
|
+
|
221
|
+
``` ruby
|
222
|
+
person = Person.first
|
223
|
+
person.pets << Pet.find(1)
|
224
|
+
person.audit_log # => ['Before linking Pet with id 1']
|
225
|
+
|
226
|
+
person.save
|
227
|
+
person.audit_log # => ['Before linking Pet with id 1']
|
228
|
+
```
|
229
|
+
|
230
|
+
As you can see, the callback method will not be called again when saving the
|
231
|
+
parent object.
|
232
|
+
|
233
|
+
Note that, instead of a `:before_link` callback, you can also use a
|
234
|
+
`before_save` callback on the Person model that calls the `link_pet` method on
|
235
|
+
each of the pets that are to be linked to the parent object.
|
236
|
+
|
237
|
+
#### Links and unlinks
|
238
|
+
|
239
|
+
In some cases, you want to know which records are going to be linked or unlinked
|
240
|
+
from the parent object. Deferring provides this information with the following
|
241
|
+
methods:
|
242
|
+
* `association.links`
|
243
|
+
* `association.unlinks`
|
244
|
+
|
245
|
+
These are aliased as `:pending_creates` and `:pending_deletes`. I am not sure if
|
246
|
+
this will be supported in the future, so do not depend on it.
|
130
247
|
|
131
|
-
|
132
|
-
original_name/checked.
|
248
|
+
An example:
|
133
249
|
|
250
|
+
Writing to the audit log is very expensive. Writing to it every time a record is
|
251
|
+
added would slow down the application. In this case, you want to write to the
|
252
|
+
audit log in bulk. Here is how you could do that using Deferring:
|
253
|
+
|
254
|
+
``` ruby
|
255
|
+
class Person < ActiveRecord::Base
|
256
|
+
deferred_has_and_belongs_to_many :pets
|
257
|
+
|
258
|
+
before_save :log_linked_pets
|
259
|
+
|
260
|
+
def log_linked_pets
|
261
|
+
ids = pending_creates.map(&:id)
|
262
|
+
audit_log << "Linking pets: #{ids.join(',')}"
|
263
|
+
end
|
264
|
+
|
265
|
+
def audit_log
|
266
|
+
@log = []
|
267
|
+
end
|
268
|
+
end
|
269
|
+
```
|
134
270
|
|
135
271
|
### How does it work?
|
136
272
|
|
@@ -148,10 +284,10 @@ avoid ;-)
|
|
148
284
|
|
149
285
|
### Gotchas
|
150
286
|
|
151
|
-
#### Using autosave (or not actually)
|
287
|
+
#### Using autosave (or not, actually)
|
152
288
|
|
153
|
-
TL;DR; Using `autosave: true` (or false) on a deferred association
|
154
|
-
|
289
|
+
TL;DR; Using `autosave: true` (or false) on a deferred association does not do
|
290
|
+
anything.
|
155
291
|
|
156
292
|
This is what the Rails documentation says about the AutosaveAssociation:
|
157
293
|
|
@@ -172,53 +308,83 @@ the array of associated records to original association. This kind of assignment
|
|
172
308
|
bypasses the autosave behaviour, see the _Why use it?_ part on top of this
|
173
309
|
README.
|
174
310
|
|
175
|
-
|
311
|
+
**TODO:** Is this correct? Or does autosave: true prevent new records from being
|
312
|
+
saved? Test.
|
176
313
|
|
177
|
-
|
314
|
+
#### Adding/removing records before saving parent
|
178
315
|
|
179
|
-
|
180
|
-
|
316
|
+
Event if using Deferring, it is still possible to add/remove a record before
|
317
|
+
saving the parent. There are two ways:
|
318
|
+
* using methods that are not mapped to the deferred associations, or
|
319
|
+
* using the original association.
|
181
320
|
|
182
|
-
|
321
|
+
##### Unmapped methods
|
183
322
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
323
|
+
As a rule, you can expect that methods defined in `Enumerable` and `Array` are
|
324
|
+
called on the deferred association. Exceptions are:
|
325
|
+
* `find`, and
|
326
|
+
* `select` (when not using a block).
|
188
327
|
|
189
|
-
|
190
|
-
|
191
|
-
|
328
|
+
Most other methods are called on the original association, most importantly:
|
329
|
+
* `create` or `create!`, and
|
330
|
+
* `destroy`, `destroy!` and `destroy_all`,
|
192
331
|
|
193
|
-
|
194
|
-
|
195
|
-
|
332
|
+
This can cause an record to be removed or added before saving the parent object.
|
333
|
+
|
334
|
+
``` ruby
|
335
|
+
class Person
|
336
|
+
deferred_has_and_belongs_to_many :teams
|
337
|
+
validates :name, presence: true
|
196
338
|
end
|
197
|
-
```
|
198
339
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
`before_adding` callback function).
|
340
|
+
class Team
|
341
|
+
has_and_belongs_to_many :people
|
342
|
+
end
|
203
343
|
|
204
|
-
|
344
|
+
person = Person.create(name: 'Bob')
|
345
|
+
person.teams.create(name: 'Support')
|
346
|
+
person.name = nil
|
347
|
+
person.save
|
348
|
+
# => false, because the name attribute is empty
|
205
349
|
|
206
|
-
|
207
|
-
|
208
|
-
person.teams << Team.find(1)
|
209
|
-
person.audit_log # => ['Before adding Team 1']
|
350
|
+
Person.first.teams
|
351
|
+
# => [#<Team id: 4, name: "Support", ... ]
|
210
352
|
```
|
211
353
|
|
212
|
-
|
213
|
-
|
354
|
+
##### Original association
|
355
|
+
|
356
|
+
The original association is renamed to "original_association_name". So, the
|
357
|
+
original association of the deferred association named `teams` can be accessed
|
358
|
+
by using `original_teams`.
|
214
359
|
|
215
360
|
``` ruby
|
216
|
-
|
217
|
-
|
218
|
-
|
361
|
+
class Person
|
362
|
+
deferred_has_and_belongs_to_many :teams
|
363
|
+
validates :name, presence: true
|
364
|
+
end
|
219
365
|
|
366
|
+
class Team
|
367
|
+
has_and_belongs_to_many :people
|
368
|
+
end
|
369
|
+
|
370
|
+
support = Team.create(name: 'Support')
|
371
|
+
person = Person.create(name: 'Bob')
|
372
|
+
|
373
|
+
person.original_teams << support
|
374
|
+
person.name = nil
|
220
375
|
person.save
|
221
|
-
|
376
|
+
# => false, because the name attribute is empty
|
377
|
+
|
378
|
+
Person.first.teams
|
379
|
+
# => [#<Team id: 4, name: "Support", ... ]
|
380
|
+
```
|
381
|
+
|
382
|
+
## Development
|
383
|
+
|
384
|
+
Run specs on all different Rails version using Appraisal:
|
385
|
+
|
386
|
+
```
|
387
|
+
bundle exec appraisal rake
|
222
388
|
```
|
223
389
|
|
224
390
|
## TODO
|
data/deferring.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
associations until the parent object is saved.
|
14
14
|
}
|
15
15
|
spec.summary = %q{Defer saving ActiveRecord associations until parent is saved}
|
16
|
-
spec.homepage = 'http://github.com/robinroestenburg/
|
16
|
+
spec.homepage = 'http://github.com/robinroestenburg/deferring'
|
17
17
|
spec.license = "MIT"
|
18
18
|
|
19
19
|
spec.files = `git ls-files`.split($/)
|
data/lib/deferring.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'deferring/version'
|
4
4
|
require 'deferring/deferred_association'
|
5
|
+
require 'deferring/deferred_callback_listener'
|
5
6
|
|
6
7
|
module Deferring
|
7
8
|
# Creates a wrapper around `has_and_belongs_to_many`. A normal habtm
|
@@ -10,8 +11,11 @@ module Deferring
|
|
10
11
|
# replaced with ones that will defer saving the association until the parent
|
11
12
|
# object has been saved.
|
12
13
|
def deferred_has_and_belongs_to_many(*args)
|
13
|
-
|
14
|
-
|
14
|
+
options = args.extract_options!
|
15
|
+
listeners = create_callback_listeners!(options)
|
16
|
+
|
17
|
+
has_and_belongs_to_many(*args, options)
|
18
|
+
generate_deferred_association_methods(args.first.to_s, listeners)
|
15
19
|
end
|
16
20
|
|
17
21
|
# Creates a wrapper around `has_many`. A normal has many association is
|
@@ -19,8 +23,11 @@ module Deferring
|
|
19
23
|
# accessor methods of the original association are replaced with ones that
|
20
24
|
# will defer saving the association until the parent object has been saved.
|
21
25
|
def deferred_has_many(*args)
|
22
|
-
|
23
|
-
|
26
|
+
options = args.extract_options!
|
27
|
+
listeners = create_callback_listeners!(options)
|
28
|
+
|
29
|
+
has_many(*args, options)
|
30
|
+
generate_deferred_association_methods(args.first.to_s, listeners)
|
24
31
|
end
|
25
32
|
|
26
33
|
def deferred_accepts_nested_attributes_for(*args)
|
@@ -29,7 +36,7 @@ module Deferring
|
|
29
36
|
|
30
37
|
# teams_attributes=
|
31
38
|
define_method :"#{association_name}_attributes=" do |records|
|
32
|
-
find_or_create_deferred_association(association_name)
|
39
|
+
find_or_create_deferred_association(association_name, [])
|
33
40
|
|
34
41
|
# Remove the records that are to be destroyed from the ids that are to be
|
35
42
|
# assigned to the DeferredAssociation instance.
|
@@ -43,7 +50,9 @@ module Deferring
|
|
43
50
|
generate_find_or_create_deferred_association_method
|
44
51
|
end
|
45
52
|
|
46
|
-
|
53
|
+
private
|
54
|
+
|
55
|
+
def generate_deferred_association_methods(association_name, listeners)
|
47
56
|
# Store the original accessor methods of the association.
|
48
57
|
alias_method :"original_#{association_name}", :"#{association_name}"
|
49
58
|
alias_method :"original_#{association_name}=", :"#{association_name}="
|
@@ -51,18 +60,13 @@ module Deferring
|
|
51
60
|
# Accessor for our own association.
|
52
61
|
attr_accessor :"deferred_#{association_name}"
|
53
62
|
|
54
|
-
# before/afer remove callbacks
|
55
|
-
define_callbacks :"deferred_#{association_name}_save", scope: [:kind, :name]
|
56
|
-
define_callbacks :"deferred_#{association_name.singularize}_remove", scope: [:kind, :name]
|
57
|
-
define_callbacks :"deferred_#{association_name.singularize}_add", scope: [:kind, :name]
|
58
|
-
|
59
63
|
# collection
|
60
64
|
#
|
61
65
|
# Returns an array of all the associated objects. An empty array is returned
|
62
66
|
# if none are found.
|
63
67
|
# TODO: add force_reload argument?
|
64
68
|
define_method :"#{association_name}" do
|
65
|
-
find_or_create_deferred_association(association_name)
|
69
|
+
find_or_create_deferred_association(association_name, listeners)
|
66
70
|
send(:"deferred_#{association_name}")
|
67
71
|
end
|
68
72
|
|
@@ -71,7 +75,7 @@ module Deferring
|
|
71
75
|
# Replaces the collection's content by deleting and adding objects as
|
72
76
|
# appropriate.
|
73
77
|
define_method :"#{association_name}=" do |objects|
|
74
|
-
find_or_create_deferred_association(association_name)
|
78
|
+
find_or_create_deferred_association(association_name, listeners)
|
75
79
|
send(:"deferred_#{association_name}").objects = objects
|
76
80
|
end
|
77
81
|
|
@@ -80,7 +84,7 @@ module Deferring
|
|
80
84
|
# Replace the collection by the objects identified by the primary keys in
|
81
85
|
# ids.
|
82
86
|
define_method :"#{association_name.singularize}_ids=" do |ids|
|
83
|
-
find_or_create_deferred_association(association_name)
|
87
|
+
find_or_create_deferred_association(association_name, listeners)
|
84
88
|
|
85
89
|
klass = self.class.reflect_on_association(:"#{association_name}").klass
|
86
90
|
objects = klass.find(ids.reject(&:blank?))
|
@@ -91,7 +95,7 @@ module Deferring
|
|
91
95
|
#
|
92
96
|
# Returns an array of the associated objects' ids.
|
93
97
|
define_method :"#{association_name.singularize}_ids" do
|
94
|
-
find_or_create_deferred_association(association_name)
|
98
|
+
find_or_create_deferred_association(association_name, listeners)
|
95
99
|
send(:"deferred_#{association_name}").ids
|
96
100
|
end
|
97
101
|
|
@@ -105,27 +109,33 @@ module Deferring
|
|
105
109
|
# the save after the parent object has been saved
|
106
110
|
after_save :"perform_deferred_#{association_name}_save!"
|
107
111
|
define_method :"perform_deferred_#{association_name}_save!" do
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
112
|
+
find_or_create_deferred_association(association_name, listeners)
|
113
|
+
|
114
|
+
# Send the objects of our delegated association to the original
|
115
|
+
# association and store the result.
|
116
|
+
send(:"original_#{association_name}=", send(:"deferred_#{association_name}").objects)
|
117
|
+
|
118
|
+
# Store the new value of the association into our delegated association.
|
119
|
+
send(
|
120
|
+
:"deferred_#{association_name}=",
|
121
|
+
DeferredAssociation.new(send(:"original_#{association_name}"), association_name))
|
122
|
+
listeners.each do |event_name, callback_method|
|
123
|
+
l = DeferredCallbackListener.new(event_name, self, callback_method)
|
124
|
+
send(:"deferred_#{association_name}").add_callback_listener(l)
|
119
125
|
end
|
120
126
|
end
|
121
127
|
|
122
128
|
define_method :"reload_with_deferred_#{association_name}" do |*args|
|
123
|
-
find_or_create_deferred_association(association_name)
|
129
|
+
find_or_create_deferred_association(association_name, listeners)
|
124
130
|
|
125
131
|
send(:"reload_without_deferred_#{association_name}", *args).tap do
|
126
132
|
send(
|
127
133
|
:"deferred_#{association_name}=",
|
128
|
-
DeferredAssociation.new(send(:"original_#{association_name}"),
|
134
|
+
DeferredAssociation.new(send(:"original_#{association_name}"), association_name))
|
135
|
+
listeners.each do |event_name, callback_method|
|
136
|
+
l = DeferredCallbackListener.new(event_name, self, callback_method)
|
137
|
+
send(:"deferred_#{association_name}").add_callback_listener(l)
|
138
|
+
end
|
129
139
|
end
|
130
140
|
end
|
131
141
|
alias_method_chain :reload, :"deferred_#{association_name}"
|
@@ -134,14 +144,26 @@ module Deferring
|
|
134
144
|
end
|
135
145
|
|
136
146
|
def generate_find_or_create_deferred_association_method
|
137
|
-
define_method :find_or_create_deferred_association do |name|
|
147
|
+
define_method :find_or_create_deferred_association do |name, listeners|
|
138
148
|
if send(:"deferred_#{name}").nil?
|
139
149
|
send(
|
140
150
|
:"deferred_#{name}=",
|
141
|
-
DeferredAssociation.new(send(:"original_#{name}"),
|
151
|
+
DeferredAssociation.new(send(:"original_#{name}"), name))
|
152
|
+
listeners.each do |event_name, callback_method|
|
153
|
+
l = DeferredCallbackListener.new(event_name, self, callback_method)
|
154
|
+
send(:"deferred_#{name}").add_callback_listener(l)
|
155
|
+
end
|
142
156
|
end
|
143
157
|
end
|
144
158
|
end
|
159
|
+
|
160
|
+
def create_callback_listeners!(options)
|
161
|
+
[:before_link, :before_unlink, :after_link, :after_unlink].map do |event_name|
|
162
|
+
callback_method = options.delete(event_name)
|
163
|
+
[event_name, callback_method] if callback_method
|
164
|
+
end.compact
|
165
|
+
end
|
166
|
+
|
145
167
|
end
|
146
168
|
|
147
169
|
ActiveRecord::Base.send(:extend, Deferring)
|
@@ -9,22 +9,15 @@ module Deferring
|
|
9
9
|
|
10
10
|
attr_reader :load_state
|
11
11
|
|
12
|
-
def initialize(original_association,
|
12
|
+
def initialize(original_association, name)
|
13
13
|
super(original_association)
|
14
14
|
@name = name
|
15
|
-
@obj = obj
|
16
15
|
@load_state = :ghost
|
17
16
|
end
|
18
|
-
|
19
17
|
alias_method :original_association, :__getobj__
|
20
18
|
|
21
19
|
delegate :to_s, :to_a, :inspect, :==, # methods undefined by SimpleDelegator
|
22
|
-
:is_a?, :as_json,
|
23
|
-
|
24
|
-
:[], :clear, :reject, :reject!, :flatten, :flatten!, :sort!,
|
25
|
-
:empty?, :size, :length, # methods on Array
|
26
|
-
|
27
|
-
to: :objects
|
20
|
+
:is_a?, :as_json, to: :objects
|
28
21
|
|
29
22
|
def each(&block)
|
30
23
|
objects.each(&block)
|
@@ -41,10 +34,26 @@ module Deferring
|
|
41
34
|
end
|
42
35
|
end
|
43
36
|
|
37
|
+
# Delegates methods from Ruby's Array module to the object in the deferred
|
38
|
+
# association.
|
39
|
+
delegate :[], :clear, :reject, :reject!, :flatten, :flatten!, :sort!,
|
40
|
+
:sort_by!, :empty?, :size, :length, to: :objects
|
41
|
+
|
42
|
+
# Delegates Ruby's Enumerable#find method to the original association.
|
43
|
+
#
|
44
|
+
# The delegation has to be explicit in this case, because the inclusion of
|
45
|
+
# Enumerable also defines the find-method on DeferredAssociation.
|
44
46
|
def find(*args)
|
45
47
|
original_association.find(*args)
|
46
48
|
end
|
47
49
|
|
50
|
+
# Delegates Ruby's Enumerable#select method to the original association when
|
51
|
+
# no block has been given. Rails' select-method does not accept a block, so
|
52
|
+
# we know that in that case the select-method has to be called on our
|
53
|
+
# deferred association.
|
54
|
+
#
|
55
|
+
# The delegation has to be explicit in this case, because the inclusion of
|
56
|
+
# Enumerable also defines the select-method on DeferredAssociation.
|
48
57
|
def select(value = Proc.new)
|
49
58
|
if block_given?
|
50
59
|
objects.select { |*block_args| value.call(*block_args) }
|
@@ -58,40 +67,18 @@ module Deferring
|
|
58
67
|
original_association.__send__(:set_inverse_instance, associated_record, parent_record)
|
59
68
|
end
|
60
69
|
|
61
|
-
def association
|
62
|
-
load_objects
|
63
|
-
original_association
|
64
|
-
end
|
65
|
-
|
66
70
|
def objects
|
67
71
|
load_objects
|
68
72
|
@objects
|
69
73
|
end
|
70
74
|
|
71
|
-
def original_objects
|
72
|
-
load_objects
|
73
|
-
@original_objects
|
74
|
-
end
|
75
|
-
|
76
75
|
def objects=(records)
|
77
76
|
@objects = records
|
78
77
|
@original_objects = original_association.to_a.clone
|
79
78
|
objects_loaded!
|
80
79
|
|
81
|
-
pending_deletes.each
|
82
|
-
|
83
|
-
# Refactor to remove that (some kind of notification), it looks
|
84
|
-
# terrible this way ;(
|
85
|
-
@obj.instance_variable_set(:"@deferred_#{@name.singularize}_remove", record)
|
86
|
-
@obj.run_callbacks :"deferred_#{@name.singularize}_remove"
|
87
|
-
@obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_remove")
|
88
|
-
end
|
89
|
-
|
90
|
-
pending_creates.each do |record|
|
91
|
-
@obj.instance_variable_set(:"@deferred_#{@name.singularize}_add", record)
|
92
|
-
@obj.run_callbacks :"deferred_#{@name.singularize}_add"
|
93
|
-
@obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_add")
|
94
|
-
end
|
80
|
+
pending_deletes.each { |record| run_deferring_callbacks(:unlink, record) }
|
81
|
+
pending_creates.each { |record| run_deferring_callbacks(:link, record) }
|
95
82
|
|
96
83
|
@objects
|
97
84
|
end
|
@@ -104,13 +91,11 @@ module Deferring
|
|
104
91
|
# TODO: Do we want to prevent including the same object twice? Not sure,
|
105
92
|
# but it will probably be filtered after saving and retrieving as well.
|
106
93
|
Array(records).flatten.uniq.each do |record|
|
107
|
-
|
108
|
-
@obj.run_callbacks :"deferred_#{@name.singularize}_add" do
|
94
|
+
run_deferring_callbacks(:link, record) do
|
109
95
|
objects << record
|
110
96
|
end
|
111
|
-
@obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_add")
|
112
97
|
end
|
113
|
-
|
98
|
+
self
|
114
99
|
end
|
115
100
|
alias_method :push, :<<
|
116
101
|
alias_method :concat, :<<
|
@@ -118,11 +103,9 @@ module Deferring
|
|
118
103
|
|
119
104
|
def delete(records)
|
120
105
|
Array(records).flatten.uniq.each do |record|
|
121
|
-
|
122
|
-
@obj.run_callbacks :"deferred_#{@name.singularize}_remove" do
|
106
|
+
run_deferring_callbacks(:unlink, record) do
|
123
107
|
objects.delete(record)
|
124
108
|
end
|
125
|
-
@obj.send(:remove_instance_variable, :"@deferred_#{@name.singularize}_remove")
|
126
109
|
end
|
127
110
|
self
|
128
111
|
end
|
@@ -130,10 +113,21 @@ module Deferring
|
|
130
113
|
def build(*args, &block)
|
131
114
|
association.build(*args, &block).tap do |result|
|
132
115
|
objects.push(result)
|
116
|
+
|
117
|
+
# Remove the newly build record from the original association. If we
|
118
|
+
# didn't do this, the new record would be saved to the database when
|
119
|
+
# saving the parent object (and not after, as we want).
|
133
120
|
association.reload
|
134
121
|
end
|
135
122
|
end
|
136
123
|
|
124
|
+
def create(*args, &block)
|
125
|
+
association.create(*args, &block).tap do |result|
|
126
|
+
@load_state = :ghost
|
127
|
+
load_objects
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
137
131
|
def create!(*args, &block)
|
138
132
|
association.create!(*args, &block).tap do |result|
|
139
133
|
@load_state = :ghost
|
@@ -150,23 +144,32 @@ module Deferring
|
|
150
144
|
|
151
145
|
# Returns the associated records to which links will be created after saving
|
152
146
|
# the parent of the association.
|
153
|
-
def
|
147
|
+
def links
|
154
148
|
return [] unless objects_loaded?
|
155
149
|
objects - original_objects
|
156
150
|
end
|
157
|
-
alias_method :
|
151
|
+
alias_method :pending_creates, :links
|
158
152
|
|
159
153
|
# Returns the associated records to which the links will be deleted after
|
160
154
|
# saving the parent of the assocation.
|
161
|
-
def
|
155
|
+
def unlinks
|
162
156
|
# TODO: Write test for it.
|
163
157
|
return [] unless objects_loaded?
|
164
158
|
original_objects - objects
|
165
159
|
end
|
166
|
-
alias_method :
|
160
|
+
alias_method :pending_deletes, :unlinks
|
161
|
+
|
162
|
+
def add_callback_listener(listener)
|
163
|
+
(@listeners ||= []) << listener
|
164
|
+
end
|
167
165
|
|
168
166
|
private
|
169
167
|
|
168
|
+
def association
|
169
|
+
load_objects
|
170
|
+
original_association
|
171
|
+
end
|
172
|
+
|
170
173
|
def load_objects
|
171
174
|
return if objects_loaded?
|
172
175
|
|
@@ -183,5 +186,24 @@ module Deferring
|
|
183
186
|
@load_state = :loaded
|
184
187
|
end
|
185
188
|
|
189
|
+
def original_objects
|
190
|
+
load_objects
|
191
|
+
@original_objects
|
192
|
+
end
|
193
|
+
|
194
|
+
def run_deferring_callbacks(event_name, record)
|
195
|
+
notify_callback_listeners(:"before_#{event_name}", record)
|
196
|
+
yield if block_given?
|
197
|
+
notify_callback_listeners(:"after_#{event_name}", record)
|
198
|
+
end
|
199
|
+
|
200
|
+
def notify_callback_listeners(event_name, record)
|
201
|
+
@listeners && @listeners.each do |listener|
|
202
|
+
if listener.event_name == event_name
|
203
|
+
listener.public_send(event_name, record)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
186
208
|
end
|
187
209
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Deferring
|
2
|
+
class DeferredCallbackListener < Struct.new(:event_name, :callee, :callback)
|
3
|
+
|
4
|
+
[:before_link, :before_unlink, :after_link, :after_unlink].each do |event_name|
|
5
|
+
define_method(event_name) do |record|
|
6
|
+
callee.public_send(callback, record)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
data/lib/deferring/version.rb
CHANGED
data/spec/lib/deferring_spec.rb
CHANGED
@@ -274,6 +274,114 @@ RSpec.describe 'deferred has-and-belongs-to-many associations' do
|
|
274
274
|
|
275
275
|
end
|
276
276
|
|
277
|
+
describe 'callbacks' do
|
278
|
+
|
279
|
+
before(:example) do
|
280
|
+
bob = Person.first
|
281
|
+
bob.teams = [Team.find(3)]
|
282
|
+
bob.save!
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'calls the link callbacks when adding a record using <<' do
|
286
|
+
bob = Person.first
|
287
|
+
bob.teams << Team.find(1)
|
288
|
+
|
289
|
+
expect(bob.audit_log.length).to eq(2)
|
290
|
+
expect(bob.audit_log).to eq([
|
291
|
+
'Before linking team 1',
|
292
|
+
'After linking team 1'
|
293
|
+
])
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'calls the link callbacks when adding a record using push' do
|
297
|
+
bob = Person.first
|
298
|
+
bob.teams.push(Team.find(1))
|
299
|
+
|
300
|
+
expect(bob.audit_log.length).to eq(2)
|
301
|
+
expect(bob.audit_log).to eq([
|
302
|
+
'Before linking team 1',
|
303
|
+
'After linking team 1'
|
304
|
+
])
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'calls the link callbacks when adding a record using append' do
|
308
|
+
bob = Person.first
|
309
|
+
bob.teams.append(Team.find(1))
|
310
|
+
|
311
|
+
expect(bob.audit_log.length).to eq(2)
|
312
|
+
expect(bob.audit_log).to eq([
|
313
|
+
'Before linking team 1',
|
314
|
+
'After linking team 1'
|
315
|
+
])
|
316
|
+
end
|
317
|
+
|
318
|
+
it 'only calls the Rails callbacks when creating a record on the association using create' do
|
319
|
+
bob = Person.first
|
320
|
+
bob.teams.create(name: 'HR')
|
321
|
+
|
322
|
+
expect(bob.audit_log.length).to eq(2)
|
323
|
+
expect(bob.audit_log).to eq([
|
324
|
+
'Before adding new team',
|
325
|
+
'After adding team 4'
|
326
|
+
])
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'only calls the Rails callbacks when creating a record on the association using create!' do
|
330
|
+
bob = Person.first
|
331
|
+
bob.teams.create!(name: 'HR')
|
332
|
+
|
333
|
+
expect(bob.audit_log.length).to eq(2)
|
334
|
+
expect(bob.audit_log).to eq([
|
335
|
+
'Before adding new team',
|
336
|
+
'After adding team 4'
|
337
|
+
])
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'calls the unlink callbacks when removing a record using delete' do
|
341
|
+
bob = Person.first
|
342
|
+
bob.teams.delete(Team.find(3))
|
343
|
+
|
344
|
+
expect(bob.audit_log.length).to eq(2)
|
345
|
+
expect(bob.audit_log).to eq([
|
346
|
+
'Before unlinking team 3',
|
347
|
+
'After unlinking team 3'
|
348
|
+
])
|
349
|
+
end
|
350
|
+
|
351
|
+
it 'only calls the rails callbacks when removing a record using destroy' do
|
352
|
+
bob = Person.first
|
353
|
+
bob.teams.destroy(3)
|
354
|
+
|
355
|
+
expect(bob.audit_log.length).to eq(2)
|
356
|
+
expect(bob.audit_log).to eq([
|
357
|
+
'Before removing team 3',
|
358
|
+
'After removing team 3'
|
359
|
+
])
|
360
|
+
end
|
361
|
+
|
362
|
+
it 'calls the regular Rails callbacks after saving' do
|
363
|
+
bob = Person.first
|
364
|
+
bob.teams = [Team.find(1), Team.find(3)]
|
365
|
+
bob.save!
|
366
|
+
|
367
|
+
bob = Person.first
|
368
|
+
bob.teams.delete(Team.find(1))
|
369
|
+
bob.teams << Team.find(2)
|
370
|
+
bob.save!
|
371
|
+
|
372
|
+
expect(bob.audit_log.length).to eq(8)
|
373
|
+
expect(bob.audit_log).to eq([
|
374
|
+
'Before unlinking team 1', 'After unlinking team 1',
|
375
|
+
'Before linking team 2', 'After linking team 2',
|
376
|
+
'Before removing team 1',
|
377
|
+
'After removing team 1',
|
378
|
+
'Before adding team 2',
|
379
|
+
'After adding team 2'
|
380
|
+
])
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
|
277
385
|
describe 'pending creates & deletes (aka links and unlinks)' do
|
278
386
|
|
279
387
|
describe 'pending creates' do
|
@@ -417,25 +525,6 @@ RSpec.describe 'deferred has-and-belongs-to-many associations' do
|
|
417
525
|
|
418
526
|
end
|
419
527
|
|
420
|
-
it 'should call before_add, after_add, before_remove, after_remove callbacks' do
|
421
|
-
bob = Person.first
|
422
|
-
bob.teams = [Team.first, Team.find(3)]
|
423
|
-
bob.save!
|
424
|
-
|
425
|
-
bob = Person.first
|
426
|
-
bob.teams.delete(bob.teams[0])
|
427
|
-
bob.teams << Team.find(2)
|
428
|
-
bob.save!
|
429
|
-
|
430
|
-
expect(bob.audit_log.length).to eq(4)
|
431
|
-
expect(bob.audit_log).to eq([
|
432
|
-
'Before removing team 1',
|
433
|
-
'After removing team 1',
|
434
|
-
'Before adding team 2',
|
435
|
-
'After adding team 2'
|
436
|
-
])
|
437
|
-
end
|
438
|
-
|
439
528
|
describe 'accepts_nested_attributes' do
|
440
529
|
# TODO: Write more tests.
|
441
530
|
it 'should mass assign' do
|
data/spec/support/models.rb
CHANGED
@@ -2,16 +2,19 @@
|
|
2
2
|
|
3
3
|
class Person < ActiveRecord::Base
|
4
4
|
|
5
|
-
deferred_has_and_belongs_to_many :teams,
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
deferred_has_and_belongs_to_many :teams, before_link: :link_team,
|
6
|
+
after_link: :linked_team,
|
7
|
+
before_unlink: :unlink_team,
|
8
|
+
after_unlink: :unlinked_team,
|
9
|
+
before_add: :add_team,
|
10
|
+
after_add: :added_team,
|
11
|
+
before_remove: :remove_team,
|
12
|
+
after_remove: :removed_team
|
9
13
|
|
10
14
|
deferred_accepts_nested_attributes_for :teams, allow_destroy: true
|
11
15
|
|
12
|
-
deferred_has_many :issues
|
13
|
-
|
14
|
-
set_callback :deferred_issue_remove, :after, :after_removing_issue
|
16
|
+
deferred_has_many :issues, before_remove: :remove_issue,
|
17
|
+
after_remove: :removed_issue
|
15
18
|
|
16
19
|
validates_presence_of :name
|
17
20
|
|
@@ -24,28 +27,48 @@ class Person < ActiveRecord::Base
|
|
24
27
|
audit_log
|
25
28
|
end
|
26
29
|
|
30
|
+
def link_team(team)
|
31
|
+
log("Before linking team #{team.id}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def linked_team(team)
|
35
|
+
log("After linking team #{team.id}")
|
36
|
+
end
|
37
|
+
|
38
|
+
def unlink_team(team)
|
39
|
+
log("Before unlinking team #{team.id}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def unlinked_team(team)
|
43
|
+
log("After unlinking team #{team.id}")
|
44
|
+
end
|
45
|
+
|
27
46
|
def add_team(team)
|
28
|
-
|
47
|
+
if team.new_record?
|
48
|
+
log("Before adding new team")
|
49
|
+
else
|
50
|
+
log("Before adding team #{team.id}")
|
51
|
+
end
|
29
52
|
end
|
30
53
|
|
31
54
|
def added_team(team)
|
32
55
|
log("After adding team #{team.id}")
|
33
56
|
end
|
34
57
|
|
35
|
-
def
|
36
|
-
log("Before removing team #{
|
58
|
+
def remove_team(team)
|
59
|
+
log("Before removing team #{team.id}")
|
37
60
|
end
|
38
61
|
|
39
|
-
def
|
40
|
-
log("After removing team #{
|
62
|
+
def removed_team(team)
|
63
|
+
log("After removing team #{team.id}")
|
41
64
|
end
|
42
65
|
|
43
|
-
def
|
44
|
-
log("Before removing issue #{
|
66
|
+
def add_issue(issue)
|
67
|
+
log("Before removing issue #{issue.id}")
|
45
68
|
end
|
46
69
|
|
47
|
-
def
|
48
|
-
log("After removing issue #{
|
70
|
+
def added_issue(issue)
|
71
|
+
log("After removing issue #{issue.id}")
|
49
72
|
end
|
50
73
|
end
|
51
74
|
|
metadata
CHANGED
@@ -1,113 +1,100 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deferring
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.3
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Robin Roestenburg
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2014-
|
11
|
+
date: 2014-07-24 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: activerecord
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">"
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '3.0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">"
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '3.0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: bundler
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- - ~>
|
31
|
+
- - "~>"
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '1.3'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- - ~>
|
38
|
+
- - "~>"
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '1.3'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: rake
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
|
-
- -
|
45
|
+
- - ">="
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '0'
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
|
-
- -
|
52
|
+
- - ">="
|
60
53
|
- !ruby/object:Gem::Version
|
61
54
|
version: '0'
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rspec
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - ">="
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '0'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - ">="
|
76
67
|
- !ruby/object:Gem::Version
|
77
68
|
version: '0'
|
78
69
|
- !ruby/object:Gem::Dependency
|
79
70
|
name: sqlite3
|
80
71
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
72
|
requirements:
|
83
|
-
- -
|
73
|
+
- - ">="
|
84
74
|
- !ruby/object:Gem::Version
|
85
75
|
version: '0'
|
86
76
|
type: :development
|
87
77
|
prerelease: false
|
88
78
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
79
|
requirements:
|
91
|
-
- -
|
80
|
+
- - ">="
|
92
81
|
- !ruby/object:Gem::Version
|
93
82
|
version: '0'
|
94
83
|
- !ruby/object:Gem::Dependency
|
95
84
|
name: appraisal
|
96
85
|
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
86
|
requirements:
|
99
|
-
- -
|
87
|
+
- - ">="
|
100
88
|
- !ruby/object:Gem::Version
|
101
89
|
version: '0'
|
102
90
|
type: :development
|
103
91
|
prerelease: false
|
104
92
|
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
93
|
requirements:
|
107
|
-
- -
|
94
|
+
- - ">="
|
108
95
|
- !ruby/object:Gem::Version
|
109
96
|
version: '0'
|
110
|
-
description:
|
97
|
+
description: "\n The Deferring gem makes it possible to defer saving ActiveRecord\n
|
111
98
|
\ associations until the parent object is saved.\n "
|
112
99
|
email:
|
113
100
|
- robin@roestenburg.io
|
@@ -115,9 +102,9 @@ executables: []
|
|
115
102
|
extensions: []
|
116
103
|
extra_rdoc_files: []
|
117
104
|
files:
|
118
|
-
- .gitignore
|
119
|
-
- .rspec
|
120
|
-
- .travis.yml
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rspec"
|
107
|
+
- ".travis.yml"
|
121
108
|
- Appraisals
|
122
109
|
- Gemfile
|
123
110
|
- LICENSE.txt
|
@@ -136,6 +123,7 @@ files:
|
|
136
123
|
- gemfiles/rails_41.gemfile.lock
|
137
124
|
- lib/deferring.rb
|
138
125
|
- lib/deferring/deferred_association.rb
|
126
|
+
- lib/deferring/deferred_callback_listener.rb
|
139
127
|
- lib/deferring/version.rb
|
140
128
|
- spec/lib/deferring_has_many_spec.rb
|
141
129
|
- spec/lib/deferring_spec.rb
|
@@ -143,30 +131,29 @@ files:
|
|
143
131
|
- spec/support/active_record.rb
|
144
132
|
- spec/support/models.rb
|
145
133
|
- spec/support/rails_versions.rb
|
146
|
-
homepage: http://github.com/robinroestenburg/
|
134
|
+
homepage: http://github.com/robinroestenburg/deferring
|
147
135
|
licenses:
|
148
136
|
- MIT
|
137
|
+
metadata: {}
|
149
138
|
post_install_message:
|
150
139
|
rdoc_options: []
|
151
140
|
require_paths:
|
152
141
|
- lib
|
153
142
|
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
-
none: false
|
155
143
|
requirements:
|
156
|
-
- -
|
144
|
+
- - ">="
|
157
145
|
- !ruby/object:Gem::Version
|
158
146
|
version: '0'
|
159
147
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
160
|
-
none: false
|
161
148
|
requirements:
|
162
|
-
- -
|
149
|
+
- - ">="
|
163
150
|
- !ruby/object:Gem::Version
|
164
151
|
version: '0'
|
165
152
|
requirements: []
|
166
153
|
rubyforge_project:
|
167
|
-
rubygems_version:
|
154
|
+
rubygems_version: 2.2.2
|
168
155
|
signing_key:
|
169
|
-
specification_version:
|
156
|
+
specification_version: 4
|
170
157
|
summary: Defer saving ActiveRecord associations until parent is saved
|
171
158
|
test_files:
|
172
159
|
- spec/lib/deferring_has_many_spec.rb
|