deferring 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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