rails-observers 0.1.0

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