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 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 models. These are:
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, `deferred_has_many` will
129
- call `has_many` in order to set up the association.
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
- **TODO:** Describe pending_creates/pending_deletes/links/unlinks/callbacks/
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 will work,
154
- but does not do anything.
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
- #### Using custom callback methods
311
+ **TODO:** Is this correct? Or does autosave: true prevent new records from being
312
+ saved? Test.
176
313
 
177
- **TODO**: This is incorrect and has to be rewritten to match code.
314
+ #### Adding/removing records before saving parent
178
315
 
179
- You can use custom callback functions. However, the callbacks for defferred
180
- associations are triggered at a different point in time.
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
- An example to illustrate:
321
+ ##### Unmapped methods
183
322
 
184
- ``` ruby
185
- class Person < ActiveRecord::Base
186
- has_and_belongs_to_many :teams, before_add: :before_adding
187
- deferred_has_and_belongs_to_many :pets, before_add: :before_adding
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
- def audit_log
190
- @log = []
191
- end
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
- def before_adding(record)
194
- audit_log << "Before adding #{record.class} with id #{record.id}"
195
- end
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
- This sets up a Person model that has a regular HABTM association with teams and
200
- that has a deferred HABTM association with pets. Each time a team or pet is
201
- added to the database a log statement is written to the audit log (using the
202
- `before_adding` callback function).
340
+ class Team
341
+ has_and_belongs_to_many :people
342
+ end
203
343
 
204
- The regular HABTM association behaves likes this:
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
- ``` ruby
207
- person = Person.first
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
- As records of deferred associations are saved to the database after saving the
213
- parent the behavior is a bit different:
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
- person = Person.first
217
- person.pets << Pet.find(1)
218
- person.audit_log # => []
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
- person.audit_log # => ['Before adding Pet 1']
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/delay_many'
16
+ spec.homepage = 'http://github.com/robinroestenburg/deferring'
17
17
  spec.license = "MIT"
18
18
 
19
19
  spec.files = `git ls-files`.split($/)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- deferring (0.0.1)
4
+ deferring (0.0.2)
5
5
  activerecord (> 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- deferring (0.0.1)
4
+ deferring (0.0.2)
5
5
  activerecord (> 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- deferring (0.0.1)
4
+ deferring (0.0.2)
5
5
  activerecord (> 3.0)
6
6
 
7
7
  GEM
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- deferring (0.0.1)
4
+ deferring (0.0.2)
5
5
  activerecord (> 3.0)
6
6
 
7
7
  GEM
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
- has_and_belongs_to_many(*args)
14
- generate_deferred_association_methods(args.first.to_s)
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
- has_many(*args)
23
- generate_deferred_association_methods(args.first.to_s)
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
- def generate_deferred_association_methods(association_name)
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
- run_callbacks :"deferred_#{association_name}_save" do
109
- find_or_create_deferred_association(association_name)
110
-
111
- # Send the objects of our delegated association to the original
112
- # association and store the result.
113
- send(:"original_#{association_name}=", send(:"deferred_#{association_name}").objects)
114
-
115
- # Store the new value of the association into our delegated association.
116
- send(
117
- :"deferred_#{association_name}=",
118
- DeferredAssociation.new(send(:"original_#{association_name}"), self, association_name))
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}"), self, 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}"), self, 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, obj, name)
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 do |record|
82
- # TODO: I don't like the fact that we know something about @obj in here.
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
- @obj.instance_variable_set(:"@deferred_#{@name.singularize}_add", record)
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
- objects
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
- @obj.instance_variable_set(:"@deferred_#{@name.singularize}_remove", record)
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 pending_creates
147
+ def links
154
148
  return [] unless objects_loaded?
155
149
  objects - original_objects
156
150
  end
157
- alias_method :links, :pending_creates
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 pending_deletes
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 :unlinks, :pending_deletes
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
@@ -1,5 +1,5 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  module Deferring
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
@@ -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
@@ -2,16 +2,19 @@
2
2
 
3
3
  class Person < ActiveRecord::Base
4
4
 
5
- deferred_has_and_belongs_to_many :teams, before_add: :add_team,
6
- after_add: :added_team
7
- set_callback :deferred_team_remove, :before, :before_removing_team
8
- set_callback :deferred_team_remove, :after, :after_removing_team
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
- set_callback :deferred_issue_remove, :before, :before_removing_issue
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
- log("Before adding team #{team.id}")
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 before_removing_team
36
- log("Before removing team #{@deferred_team_remove.id}")
58
+ def remove_team(team)
59
+ log("Before removing team #{team.id}")
37
60
  end
38
61
 
39
- def after_removing_team
40
- log("After removing team #{@deferred_team_remove.id}")
62
+ def removed_team(team)
63
+ log("After removing team #{team.id}")
41
64
  end
42
65
 
43
- def before_adding_issue
44
- log("Before removing issue #{@deferred_issue_add.id}")
66
+ def add_issue(issue)
67
+ log("Before removing issue #{issue.id}")
45
68
  end
46
69
 
47
- def after_adding_issue
48
- log("After removing issue #{@deferred_issue_add.id}")
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.2
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-06-12 00:00:00.000000000 Z
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: ! "\n The Deferring gem makes it possible to defer saving ActiveRecord\n
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/delay_many
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: 1.8.23
154
+ rubygems_version: 2.2.2
168
155
  signing_key:
169
- specification_version: 3
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