rails-observers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +22 -0
  4. data/README.md +102 -0
  5. data/Rakefile +34 -0
  6. data/lib/generators/active_record/observer/observer_generator.rb +17 -0
  7. data/lib/generators/active_record/observer/templates/observer.rb +4 -0
  8. data/lib/generators/rails/observer/USAGE +12 -0
  9. data/lib/generators/rails/observer/observer_generator.rb +7 -0
  10. data/lib/generators/test_unit/observer/observer_generator.rb +15 -0
  11. data/lib/generators/test_unit/observer/templates/unit_test.rb +9 -0
  12. data/lib/rails-observers.rb +30 -0
  13. data/lib/rails/observers/action_controller/caching.rb +12 -0
  14. data/lib/rails/observers/action_controller/caching/sweeping.rb +113 -0
  15. data/lib/rails/observers/active_model/active_model.rb +4 -0
  16. data/lib/rails/observers/active_model/observer_array.rb +152 -0
  17. data/lib/rails/observers/active_model/observing.rb +374 -0
  18. data/lib/rails/observers/activerecord/active_record.rb +5 -0
  19. data/lib/rails/observers/activerecord/base.rb +8 -0
  20. data/lib/rails/observers/activerecord/observer.rb +125 -0
  21. data/lib/rails/observers/version.rb +5 -0
  22. data/rails-observers.gemspec +26 -0
  23. data/test/configuration_test.rb +37 -0
  24. data/test/console_test.rb +38 -0
  25. data/test/fixtures/developers.yml +4 -0
  26. data/test/fixtures/minimalistics.yml +2 -0
  27. data/test/fixtures/topics.yml +41 -0
  28. data/test/generators/generators_test_helper.rb +16 -0
  29. data/test/generators/namespaced_generators_test.rb +34 -0
  30. data/test/generators/observer_generator_test.rb +33 -0
  31. data/test/helper.rb +74 -0
  32. data/test/isolation/abstract_unit.rb +108 -0
  33. data/test/lifecycle_test.rb +249 -0
  34. data/test/models/observers.rb +27 -0
  35. data/test/observer_array_test.rb +222 -0
  36. data/test/observing_test.rb +183 -0
  37. data/test/rake_test.rb +40 -0
  38. data/test/sweeper_test.rb +83 -0
  39. data/test/transaction_callbacks_test.rb +278 -0
  40. metadata +216 -0
@@ -0,0 +1,374 @@
1
+ require 'singleton'
2
+ require 'rails/observers/active_model/observer_array'
3
+ require 'active_support/core_ext/module/aliasing'
4
+ require 'active_support/core_ext/module/remove_method'
5
+ require 'active_support/core_ext/string/inflections'
6
+ require 'active_support/core_ext/enumerable'
7
+ require 'active_support/core_ext/object/try'
8
+ require 'active_support/descendants_tracker'
9
+
10
+ module ActiveModel
11
+ # == Active Model Observers Activation
12
+ module Observing
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ extend ActiveSupport::DescendantsTracker
17
+ end
18
+
19
+ module ClassMethods
20
+ # Activates the observers assigned.
21
+ #
22
+ # class ORM
23
+ # include ActiveModel::Observing
24
+ # end
25
+ #
26
+ # # Calls PersonObserver.instance
27
+ # ORM.observers = :person_observer
28
+ #
29
+ # # Calls Cacher.instance and GarbageCollector.instance
30
+ # ORM.observers = :cacher, :garbage_collector
31
+ #
32
+ # # Same as above, just using explicit class references
33
+ # ORM.observers = Cacher, GarbageCollector
34
+ #
35
+ # Note: Setting this does not instantiate the observers yet.
36
+ # <tt>instantiate_observers</tt> is called during startup, and before
37
+ # each development request.
38
+ def observers=(*values)
39
+ observers.replace(values.flatten)
40
+ end
41
+
42
+ # Gets an array of observers observing this model. The array also provides
43
+ # +enable+ and +disable+ methods that allow you to selectively enable and
44
+ # disable observers (see ActiveModel::ObserverArray.enable and
45
+ # ActiveModel::ObserverArray.disable for more on this).
46
+ #
47
+ # class ORM
48
+ # include ActiveModel::Observing
49
+ # end
50
+ #
51
+ # ORM.observers = :cacher, :garbage_collector
52
+ # ORM.observers # => [:cacher, :garbage_collector]
53
+ # ORM.observers.class # => ActiveModel::ObserverArray
54
+ def observers
55
+ @observers ||= ObserverArray.new(self)
56
+ end
57
+
58
+ # Returns the current observer instances.
59
+ #
60
+ # class Foo
61
+ # include ActiveModel::Observing
62
+ #
63
+ # attr_accessor :status
64
+ # end
65
+ #
66
+ # class FooObserver < ActiveModel::Observer
67
+ # def on_spec(record, *args)
68
+ # record.status = true
69
+ # end
70
+ # end
71
+ #
72
+ # Foo.observers = FooObserver
73
+ # Foo.instantiate_observers
74
+ #
75
+ # Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>]
76
+ def observer_instances
77
+ @observer_instances ||= []
78
+ end
79
+
80
+ # Instantiate the global observers.
81
+ #
82
+ # class Foo
83
+ # include ActiveModel::Observing
84
+ #
85
+ # attr_accessor :status
86
+ # end
87
+ #
88
+ # class FooObserver < ActiveModel::Observer
89
+ # def on_spec(record, *args)
90
+ # record.status = true
91
+ # end
92
+ # end
93
+ #
94
+ # Foo.observers = FooObserver
95
+ #
96
+ # foo = Foo.new
97
+ # foo.status = false
98
+ # foo.notify_observers(:on_spec)
99
+ # foo.status # => false
100
+ #
101
+ # Foo.instantiate_observers # => [FooObserver]
102
+ #
103
+ # foo = Foo.new
104
+ # foo.status = false
105
+ # foo.notify_observers(:on_spec)
106
+ # foo.status # => true
107
+ def instantiate_observers
108
+ observers.each { |o| instantiate_observer(o) }
109
+ end
110
+
111
+ # Add a new observer to the pool. The new observer needs to respond to
112
+ # <tt>update</tt>, otherwise it raises an +ArgumentError+ exception.
113
+ #
114
+ # class Foo
115
+ # include ActiveModel::Observing
116
+ # end
117
+ #
118
+ # class FooObserver < ActiveModel::Observer
119
+ # end
120
+ #
121
+ # Foo.add_observer(FooObserver.instance)
122
+ #
123
+ # Foo.observers_instance
124
+ # # => [#<FooObserver:0x007fccf55d9390>]
125
+ def add_observer(observer)
126
+ unless observer.respond_to? :update
127
+ raise ArgumentError, "observer needs to respond to 'update'"
128
+ end
129
+ observer_instances << observer
130
+ end
131
+
132
+ # Fires notifications to model's observers.
133
+ #
134
+ # def save
135
+ # notify_observers(:before_save)
136
+ # ...
137
+ # notify_observers(:after_save)
138
+ # end
139
+ #
140
+ # Custom notifications can be sent in a similar fashion:
141
+ #
142
+ # notify_observers(:custom_notification, :foo)
143
+ #
144
+ # This will call <tt>custom_notification</tt>, passing as arguments
145
+ # the current object and <tt>:foo</tt>.
146
+ def notify_observers(*args)
147
+ observer_instances.each { |observer| observer.update(*args) }
148
+ end
149
+
150
+ # Returns the total number of instantiated observers.
151
+ #
152
+ # class Foo
153
+ # include ActiveModel::Observing
154
+ #
155
+ # attr_accessor :status
156
+ # end
157
+ #
158
+ # class FooObserver < ActiveModel::Observer
159
+ # def on_spec(record, *args)
160
+ # record.status = true
161
+ # end
162
+ # end
163
+ #
164
+ # Foo.observers = FooObserver
165
+ # Foo.observers_count # => 0
166
+ # Foo.instantiate_observers
167
+ # Foo.observers_count # => 1
168
+ def observers_count
169
+ observer_instances.size
170
+ end
171
+
172
+ # <tt>count_observers</tt> is deprecated. Use #observers_count.
173
+ def count_observers
174
+ msg = "count_observers is deprecated in favor of observers_count"
175
+ ActiveSupport::Deprecation.warn(msg)
176
+ observers_count
177
+ end
178
+
179
+ protected
180
+ def instantiate_observer(observer) #:nodoc:
181
+ # string/symbol
182
+ if observer.respond_to?(:to_sym)
183
+ observer = observer.to_s.camelize.constantize
184
+ end
185
+ if observer.respond_to?(:instance)
186
+ observer.instance
187
+ else
188
+ raise ArgumentError,
189
+ "#{observer} must be a lowercase, underscored class name (or " +
190
+ "the class itself) responding to the method :instance. " +
191
+ "Example: Person.observers = :big_brother # calls " +
192
+ "BigBrother.instance"
193
+ end
194
+ end
195
+
196
+ # Notify observers when the observed class is subclassed.
197
+ def inherited(subclass) #:nodoc:
198
+ super
199
+ notify_observers :observed_class_inherited, subclass
200
+ end
201
+ end
202
+
203
+ # Notify a change to the list of observers.
204
+ #
205
+ # class Foo
206
+ # include ActiveModel::Observing
207
+ #
208
+ # attr_accessor :status
209
+ # end
210
+ #
211
+ # class FooObserver < ActiveModel::Observer
212
+ # def on_spec(record, *args)
213
+ # record.status = true
214
+ # end
215
+ # end
216
+ #
217
+ # Foo.observers = FooObserver
218
+ # Foo.instantiate_observers # => [FooObserver]
219
+ #
220
+ # foo = Foo.new
221
+ # foo.status = false
222
+ # foo.notify_observers(:on_spec)
223
+ # foo.status # => true
224
+ #
225
+ # See ActiveModel::Observing::ClassMethods.notify_observers for more
226
+ # information.
227
+ def notify_observers(method, *extra_args)
228
+ self.class.notify_observers(method, self, *extra_args)
229
+ end
230
+ end
231
+
232
+ # == Active Model Observers
233
+ #
234
+ # Observer classes respond to life cycle callbacks to implement trigger-like
235
+ # behavior outside the original class. This is a great way to reduce the
236
+ # clutter that normally comes when the model class is burdened with
237
+ # functionality that doesn't pertain to the core responsibility of the
238
+ # class.
239
+ #
240
+ # class CommentObserver < ActiveModel::Observer
241
+ # def after_save(comment)
242
+ # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver
243
+ # end
244
+ # end
245
+ #
246
+ # This Observer sends an email when a <tt>Comment#save</tt> is finished.
247
+ #
248
+ # class ContactObserver < ActiveModel::Observer
249
+ # def after_create(contact)
250
+ # contact.logger.info('New contact added!')
251
+ # end
252
+ #
253
+ # def after_destroy(contact)
254
+ # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
255
+ # end
256
+ # end
257
+ #
258
+ # This Observer uses logger to log when specific callbacks are triggered.
259
+ #
260
+ # == Observing a class that can't be inferred
261
+ #
262
+ # Observers will by default be mapped to the class with which they share a
263
+ # name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>,
264
+ # <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If
265
+ # you want to name your observer differently than the class you're interested
266
+ # in observing, you can use the <tt>Observer.observe</tt> class method which
267
+ # takes either the concrete class (<tt>Product</tt>) or a symbol for that
268
+ # class (<tt>:product</tt>):
269
+ #
270
+ # class AuditObserver < ActiveModel::Observer
271
+ # observe :account
272
+ #
273
+ # def after_update(account)
274
+ # AuditTrail.new(account, 'UPDATED')
275
+ # end
276
+ # end
277
+ #
278
+ # If the audit observer needs to watch more than one kind of object, this can
279
+ # be specified with multiple arguments:
280
+ #
281
+ # class AuditObserver < ActiveModel::Observer
282
+ # observe :account, :balance
283
+ #
284
+ # def after_update(record)
285
+ # AuditTrail.new(record, 'UPDATED')
286
+ # end
287
+ # end
288
+ #
289
+ # The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt>
290
+ # and <tt>Balance</tt> by treating them both as records.
291
+ #
292
+ # If you're using an Observer in a Rails application with Active Record, be
293
+ # sure to read about the necessary configuration in the documentation for
294
+ # ActiveRecord::Observer.
295
+ class Observer
296
+ include Singleton
297
+ extend ActiveSupport::DescendantsTracker
298
+
299
+ class << self
300
+ # Attaches the observer to the supplied model classes.
301
+ #
302
+ # class AuditObserver < ActiveModel::Observer
303
+ # observe :account, :balance
304
+ # end
305
+ #
306
+ # AuditObserver.observed_classes # => [Account, Balance]
307
+ def observe(*models)
308
+ models.flatten!
309
+ models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
310
+ singleton_class.redefine_method(:observed_classes) { models }
311
+ end
312
+
313
+ # Returns an array of Classes to observe.
314
+ #
315
+ # AccountObserver.observed_classes # => [Account]
316
+ #
317
+ # You can override this instead of using the +observe+ helper.
318
+ #
319
+ # class AuditObserver < ActiveModel::Observer
320
+ # def self.observed_classes
321
+ # [Account, Balance]
322
+ # end
323
+ # end
324
+ def observed_classes
325
+ Array(observed_class)
326
+ end
327
+
328
+ # Returns the class observed by default. It's inferred from the observer's
329
+ # class name.
330
+ #
331
+ # PersonObserver.observed_class # => Person
332
+ # AccountObserver.observed_class # => Account
333
+ def observed_class
334
+ name[/(.*)Observer/, 1].try :constantize
335
+ end
336
+ end
337
+
338
+ # Start observing the declared classes and their subclasses.
339
+ # Called automatically by the instance method.
340
+ def initialize #:nodoc:
341
+ observed_classes.each { |klass| add_observer!(klass) }
342
+ end
343
+
344
+ def observed_classes #:nodoc:
345
+ self.class.observed_classes
346
+ end
347
+
348
+ # Send observed_method(object) if the method exists and
349
+ # the observer is enabled for the given object's class.
350
+ def update(observed_method, object, *extra_args, &block) #:nodoc:
351
+ return if !respond_to?(observed_method) || disabled_for?(object)
352
+ send(observed_method, object, *extra_args, &block)
353
+ end
354
+
355
+ # Special method sent by the observed class when it is inherited.
356
+ # Passes the new subclass.
357
+ def observed_class_inherited(subclass) #:nodoc:
358
+ self.class.observe(observed_classes + [subclass])
359
+ add_observer!(subclass)
360
+ end
361
+
362
+ protected
363
+ def add_observer!(klass) #:nodoc:
364
+ klass.add_observer(self)
365
+ end
366
+
367
+ # Returns true if notifications are disabled for this object.
368
+ def disabled_for?(object) #:nodoc:
369
+ klass = object.class
370
+ return false unless klass.respond_to?(:observers)
371
+ klass.observers.disabled_for?(self)
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails/observers/activerecord/base'
2
+
3
+ module ActiveRecord
4
+ autoload :Observer, 'rails/observers/activerecord/observer'
5
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails/observers/active_model/active_model'
2
+
3
+ module ActiveRecord
4
+ class Base
5
+ extend ActiveModel::Observing::ClassMethods
6
+ include ActiveModel::Observing
7
+ end
8
+ end
@@ -0,0 +1,125 @@
1
+ module ActiveRecord
2
+ # = Active Record Observer
3
+ #
4
+ # Observer classes respond to life cycle callbacks to implement trigger-like
5
+ # behavior outside the original class. This is a great way to reduce the
6
+ # clutter that normally comes when the model class is burdened with
7
+ # functionality that doesn't pertain to the core responsibility of the
8
+ # class. Example:
9
+ #
10
+ # class CommentObserver < ActiveRecord::Observer
11
+ # def after_save(comment)
12
+ # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
13
+ # end
14
+ # end
15
+ #
16
+ # This Observer sends an email when a Comment#save is finished.
17
+ #
18
+ # class ContactObserver < ActiveRecord::Observer
19
+ # def after_create(contact)
20
+ # contact.logger.info('New contact added!')
21
+ # end
22
+ #
23
+ # def after_destroy(contact)
24
+ # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
25
+ # end
26
+ # end
27
+ #
28
+ # This Observer uses logger to log when specific callbacks are triggered.
29
+ #
30
+ # == Observing a class that can't be inferred
31
+ #
32
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
33
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
34
+ # differently than the class you're interested in observing, you can use the Observer.observe class method which takes
35
+ # either the concrete class (Product) or a symbol for that class (:product):
36
+ #
37
+ # class AuditObserver < ActiveRecord::Observer
38
+ # observe :account
39
+ #
40
+ # def after_update(account)
41
+ # AuditTrail.new(account, "UPDATED")
42
+ # end
43
+ # end
44
+ #
45
+ # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
46
+ #
47
+ # class AuditObserver < ActiveRecord::Observer
48
+ # observe :account, :balance
49
+ #
50
+ # def after_update(record)
51
+ # AuditTrail.new(record, "UPDATED")
52
+ # end
53
+ # end
54
+ #
55
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
56
+ #
57
+ # == Available callback methods
58
+ #
59
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
60
+ #
61
+ # == Storing Observers in Rails
62
+ #
63
+ # If you're using Active Record within Rails, observer classes are usually stored in app/models with the
64
+ # naming convention of app/models/audit_observer.rb.
65
+ #
66
+ # == Configuration
67
+ #
68
+ # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
69
+ # setting in your <tt>config/application.rb</tt> file.
70
+ #
71
+ # config.active_record.observers = :comment_observer, :signup_observer
72
+ #
73
+ # Observers will not be invoked unless you define these in your application configuration.
74
+ #
75
+ # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
76
+ # environment file:
77
+ #
78
+ # ActiveRecord::Base.add_observer CommentObserver.instance
79
+ # ActiveRecord::Base.add_observer SignupObserver.instance
80
+ #
81
+ # == Loading
82
+ #
83
+ # Observers register themselves in the model class they observe, since it is the class that
84
+ # notifies them of events when they occur. As a side-effect, when an observer is loaded its
85
+ # corresponding model class is loaded.
86
+ #
87
+ # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
88
+ # application initializers. Now observers are loaded after application initializers,
89
+ # so observed models can make use of extensions.
90
+ #
91
+ # If by any chance you are using observed models in the initialization you can still
92
+ # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
93
+ # singletons and that call instantiates and registers them.
94
+ #
95
+ class Observer < ActiveModel::Observer
96
+
97
+ protected
98
+
99
+ def observed_classes
100
+ klasses = super
101
+ klasses + klasses.map { |klass| klass.descendants }.flatten
102
+ end
103
+
104
+ def add_observer!(klass)
105
+ super
106
+ define_callbacks klass
107
+ end
108
+
109
+ def define_callbacks(klass)
110
+ observer = self
111
+ observer_name = observer.class.name.underscore.gsub('/', '__')
112
+
113
+ ActiveRecord::Callbacks::CALLBACKS.each do |callback|
114
+ next unless respond_to?(callback)
115
+ callback_meth = :"_notify_#{observer_name}_for_#{callback}"
116
+ unless klass.respond_to?(callback_meth)
117
+ klass.send(:define_method, callback_meth) do |&block|
118
+ observer.update(callback, self, &block)
119
+ end
120
+ klass.send(callback, callback_meth)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end