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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_record-observers.gemspec
4
+ gemspec
5
+
6
+ gem 'activerecord-deprecated_finders', git: 'git://github.com/rails/activerecord-deprecated_finders'
7
+ gem 'journey', git: 'https://github.com/rails/journey.git'
8
+
9
+ gem 'rails', git: 'git://github.com/rafaelfranca/rails', branch: 'extract_observers'
10
+
11
+ gem 'mocha', require: false
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Steve Klabnik, Rafael Mendonça França
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,102 @@
1
+ # Rails::Observers
2
+
3
+ Rails Observers (removed from core in Rails 4.0)
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'rails-observers'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install rails-observers
18
+
19
+ ## Usage
20
+
21
+ This gem contains two observers:
22
+
23
+ * Active Record Observer
24
+ * Action Controller Sweeper
25
+
26
+ ### Active Record Observer
27
+
28
+ Observer classes respond to life cycle callbacks to implement trigger-like
29
+ behavior outside the original class. This is a great way to reduce the
30
+ clutter that normally comes when the model class is burdened with
31
+ functionality that doesn't pertain to the core responsibility of the
32
+ class. Example:
33
+
34
+ ```ruby
35
+ class CommentObserver < ActiveRecord::Observer
36
+ def after_save(comment)
37
+ Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
38
+ end
39
+ end
40
+ ```
41
+
42
+ This Observer sends an email when a Comment#save is finished.
43
+
44
+ ```ruby
45
+ class ContactObserver < ActiveRecord::Observer
46
+ def after_create(contact)
47
+ contact.logger.info('New contact added!')
48
+ end
49
+
50
+ def after_destroy(contact)
51
+ contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
52
+ end
53
+ end
54
+ ```
55
+
56
+ This Observer uses logger to log when specific callbacks are triggered.
57
+
58
+ ### Action Controller Sweeper
59
+
60
+ Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
61
+ They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
62
+
63
+ ```ruby
64
+ class ListSweeper < ActionController::Caching::Sweeper
65
+ observe List, Item
66
+
67
+ def after_save(record)
68
+ list = record.is_a?(List) ? record : record.list
69
+ expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
70
+ expire_action(:controller => "lists", :action => "all")
71
+ list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
72
+ end
73
+ end
74
+ ```
75
+
76
+ The sweeper is assigned in the controllers that wish to have its job performed using the `cache_sweeper` class method:
77
+
78
+ ```ruby
79
+ class ListsController < ApplicationController
80
+ caches_action :index, :show, :public, :feed
81
+ cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
82
+ end
83
+ ```
84
+
85
+ In the example above, four actions are cached and three actions are responsible for expiring those caches.
86
+
87
+ You can also name an explicit class in the declaration of a sweeper, which is needed if the sweeper is in a module:
88
+
89
+ ```ruby
90
+ class ListsController < ApplicationController
91
+ caches_action :index, :show, :public, :feed
92
+ cache_sweeper OpenBar::Sweeper, :only => [ :edit, :destroy, :share ]
93
+ end
94
+ ```
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create new Pull Request
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new("test:regular") do |t|
7
+ t.libs = ["test"]
8
+ t.pattern = "test/*_test.rb"
9
+ t.ruby_opts = ['-w']
10
+ end
11
+
12
+ Rake::TestTask.new("test:generators") do |t|
13
+ t.libs = ["test"]
14
+ t.pattern = "test/generators/*_test.rb"
15
+ t.ruby_opts = ['-w']
16
+ end
17
+
18
+ task :default => :test
19
+ task :test => ['test:regular', 'test:generators']
20
+
21
+ specname = "rails-observers.gemspec"
22
+ deps = `git ls-files`.split("\n") - [specname]
23
+
24
+ file specname => deps do
25
+ files = `git ls-files`.split("\n")
26
+ test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+
29
+ require 'erb'
30
+
31
+ File.open specname, 'w:utf-8' do |f|
32
+ f.write ERB.new(File.read("#{specname}.erb")).result(binding)
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class ObserverGenerator < Base
6
+ check_class_collision :suffix => "Observer"
7
+
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def create_observer_file
11
+ template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb")
12
+ end
13
+
14
+ hook_for :test_framework
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Observer < ActiveRecord::Observer
3
+ end
4
+ <% end -%>
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Stubs out a new observer. Pass the observer name, either CamelCased or
3
+ under_scored, as an argument.
4
+
5
+ This generator only invokes your ORM and test framework generators.
6
+
7
+ Example:
8
+ `rails generate observer Account`
9
+
10
+ For ActiveRecord and TestUnit it creates:
11
+ Observer: app/models/account_observer.rb
12
+ TestUnit: test/unit/account_observer_test.rb
@@ -0,0 +1,7 @@
1
+ module Rails
2
+ module Generators
3
+ class ObserverGenerator < NamedBase #metagenerator
4
+ hook_for :orm, :required => true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/test_unit'
2
+
3
+ module TestUnit
4
+ module Generators
5
+ class ObserverGenerator < Base
6
+ check_class_collision :suffix => "ObserverTest"
7
+
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def create_test_files
11
+ template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_observer_test.rb")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>ObserverTest < ActiveSupport::TestCase
5
+ # test "the truth" do
6
+ # assert true
7
+ # end
8
+ end
9
+ <% end -%>
@@ -0,0 +1,30 @@
1
+ require 'rails'
2
+ require 'rails/observers/version'
3
+
4
+ module Rails
5
+ module Observers
6
+ class Railtie < ::Rails::Railtie
7
+ initializer "active_record.observer", :before => "active_record.set_configs" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ require "rails/observers/activerecord/active_record"
10
+ end
11
+ end
12
+
13
+ initializer "action_controller.caching.sweppers" do
14
+ ActiveSupport.on_load(:action_controller) do
15
+ require "rails/observers/action_controller/caching"
16
+ end
17
+ end
18
+
19
+ config.after_initialize do |app|
20
+ ActiveSupport.on_load(:active_record) do
21
+ ActiveRecord::Base.instantiate_observers
22
+
23
+ ActionDispatch::Reloader.to_prepare do
24
+ ActiveRecord::Base.instantiate_observers
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module ActionController #:nodoc:
2
+ module Caching
3
+ extend ActiveSupport::Autoload
4
+
5
+ eager_autoload do
6
+ autoload :Sweeper, 'rails/observers/action_controller/caching/sweeping'
7
+ autoload :Sweeping, 'rails/observers/action_controller/caching/sweeping'
8
+ end
9
+
10
+ include Sweeping if defined?(ActiveRecord)
11
+ end
12
+ end
@@ -0,0 +1,113 @@
1
+ module ActionController #:nodoc:
2
+ module Caching
3
+ # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
4
+ # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
5
+ #
6
+ # class ListSweeper < ActionController::Caching::Sweeper
7
+ # observe List, Item
8
+ #
9
+ # def after_save(record)
10
+ # list = record.is_a?(List) ? record : record.list
11
+ # expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
12
+ # expire_action(:controller => "lists", :action => "all")
13
+ # list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
14
+ # end
15
+ # end
16
+ #
17
+ # The sweeper is assigned in the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
18
+ #
19
+ # class ListsController < ApplicationController
20
+ # caches_action :index, :show, :public, :feed
21
+ # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
22
+ # end
23
+ #
24
+ # In the example above, four actions are cached and three actions are responsible for expiring those caches.
25
+ #
26
+ # You can also name an explicit class in the declaration of a sweeper, which is needed if the sweeper is in a module:
27
+ #
28
+ # class ListsController < ApplicationController
29
+ # caches_action :index, :show, :public, :feed
30
+ # cache_sweeper OpenBar::Sweeper, :only => [ :edit, :destroy, :share ]
31
+ # end
32
+ module Sweeping
33
+ extend ActiveSupport::Concern
34
+
35
+ module ClassMethods #:nodoc:
36
+ def cache_sweeper(*sweepers)
37
+ configuration = sweepers.extract_options!
38
+
39
+ sweepers.each do |sweeper|
40
+ ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
41
+ sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance
42
+
43
+ if sweeper_instance.is_a?(Sweeper)
44
+ around_filter(sweeper_instance, :only => configuration[:only])
45
+ else
46
+ after_filter(sweeper_instance, :only => configuration[:only])
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
54
+ class Sweeper < ActiveRecord::Observer #:nodoc:
55
+ attr_accessor :controller
56
+
57
+ def initialize(*args)
58
+ super
59
+ @controller = nil
60
+ end
61
+
62
+ def before(controller)
63
+ self.controller = controller
64
+ callback(:before) if controller.perform_caching
65
+ true # before method from sweeper should always return true
66
+ end
67
+
68
+ def after(controller)
69
+ self.controller = controller
70
+ callback(:after) if controller.perform_caching
71
+ end
72
+
73
+ def around(controller)
74
+ before(controller)
75
+ yield
76
+ after(controller)
77
+ ensure
78
+ clean_up
79
+ end
80
+
81
+ protected
82
+ # gets the action cache path for the given options.
83
+ def action_path_for(options)
84
+ Actions::ActionCachePath.new(controller, options).path
85
+ end
86
+
87
+ # Retrieve instance variables set in the controller.
88
+ def assigns(key)
89
+ controller.instance_variable_get("@#{key}")
90
+ end
91
+
92
+ private
93
+ def clean_up
94
+ # Clean up, so that the controller can be collected after this request
95
+ self.controller = nil
96
+ end
97
+
98
+ def callback(timing)
99
+ controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
100
+ action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
101
+
102
+ __send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
103
+ __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
104
+ end
105
+
106
+ def method_missing(method, *arguments, &block)
107
+ return super unless @controller
108
+ @controller.__send__(method, *arguments, &block)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveModel
2
+ autoload :Observer, 'rails/observers/active_model/observing'
3
+ autoload :Observing, 'rails/observers/active_model/observing'
4
+ end
@@ -0,0 +1,152 @@
1
+ require 'set'
2
+
3
+ module ActiveModel
4
+ # Stores the enabled/disabled state of individual observers for
5
+ # a particular model class.
6
+ class ObserverArray < Array
7
+ attr_reader :model_class
8
+ def initialize(model_class, *args) #:nodoc:
9
+ @model_class = model_class
10
+ super(*args)
11
+ end
12
+
13
+ # Returns +true+ if the given observer is disabled for the model class,
14
+ # +false+ otherwise.
15
+ def disabled_for?(observer) #:nodoc:
16
+ disabled_observers.include?(observer.class)
17
+ end
18
+
19
+ # Disables one or more observers. This supports multiple forms:
20
+ #
21
+ # ORM.observers.disable :all
22
+ # # => disables all observers for all models subclassed from
23
+ # # an ORM base class that includes ActiveModel::Observing
24
+ # # e.g. ActiveRecord::Base
25
+ #
26
+ # ORM.observers.disable :user_observer
27
+ # # => disables the UserObserver
28
+ #
29
+ # User.observers.disable AuditTrail
30
+ # # => disables the AuditTrail observer for User notifications.
31
+ # # Other models will still notify the AuditTrail observer.
32
+ #
33
+ # ORM.observers.disable :observer_1, :observer_2
34
+ # # => disables Observer1 and Observer2 for all models.
35
+ #
36
+ # User.observers.disable :all do
37
+ # # all user observers are disabled for
38
+ # # just the duration of the block
39
+ # end
40
+ def disable(*observers, &block)
41
+ set_enablement(false, observers, &block)
42
+ end
43
+
44
+ # Enables one or more observers. This supports multiple forms:
45
+ #
46
+ # ORM.observers.enable :all
47
+ # # => enables all observers for all models subclassed from
48
+ # # an ORM base class that includes ActiveModel::Observing
49
+ # # e.g. ActiveRecord::Base
50
+ #
51
+ # ORM.observers.enable :user_observer
52
+ # # => enables the UserObserver
53
+ #
54
+ # User.observers.enable AuditTrail
55
+ # # => enables the AuditTrail observer for User notifications.
56
+ # # Other models will not be affected (i.e. they will not
57
+ # # trigger notifications to AuditTrail if previously disabled)
58
+ #
59
+ # ORM.observers.enable :observer_1, :observer_2
60
+ # # => enables Observer1 and Observer2 for all models.
61
+ #
62
+ # User.observers.enable :all do
63
+ # # all user observers are enabled for
64
+ # # just the duration of the block
65
+ # end
66
+ #
67
+ # Note: all observers are enabled by default. This method is only
68
+ # useful when you have previously disabled one or more observers.
69
+ def enable(*observers, &block)
70
+ set_enablement(true, observers, &block)
71
+ end
72
+
73
+ protected
74
+
75
+ def disabled_observers #:nodoc:
76
+ @disabled_observers ||= Set.new
77
+ end
78
+
79
+ def observer_class_for(observer) #:nodoc:
80
+ return observer if observer.is_a?(Class)
81
+
82
+ if observer.respond_to?(:to_sym) # string/symbol
83
+ observer.to_s.camelize.constantize
84
+ else
85
+ raise ArgumentError, "#{observer} was not a class or a " +
86
+ "lowercase, underscored class name as expected."
87
+ end
88
+ end
89
+
90
+ def start_transaction #:nodoc:
91
+ disabled_observer_stack.push(disabled_observers.dup)
92
+ each_subclass_array do |array|
93
+ array.start_transaction
94
+ end
95
+ end
96
+
97
+ def disabled_observer_stack #:nodoc:
98
+ @disabled_observer_stack ||= []
99
+ end
100
+
101
+ def end_transaction #:nodoc:
102
+ @disabled_observers = disabled_observer_stack.pop
103
+ each_subclass_array do |array|
104
+ array.end_transaction
105
+ end
106
+ end
107
+
108
+ def transaction #:nodoc:
109
+ start_transaction
110
+
111
+ begin
112
+ yield
113
+ ensure
114
+ end_transaction
115
+ end
116
+ end
117
+
118
+ def each_subclass_array #:nodoc:
119
+ model_class.descendants.each do |subclass|
120
+ yield subclass.observers
121
+ end
122
+ end
123
+
124
+ def set_enablement(enabled, observers) #:nodoc:
125
+ if block_given?
126
+ transaction do
127
+ set_enablement(enabled, observers)
128
+ yield
129
+ end
130
+ else
131
+ observers = ActiveModel::Observer.descendants if observers == [:all]
132
+ observers.each do |obs|
133
+ klass = observer_class_for(obs)
134
+
135
+ unless klass < ActiveModel::Observer
136
+ raise ArgumentError.new("#{obs} does not refer to a valid observer")
137
+ end
138
+
139
+ if enabled
140
+ disabled_observers.delete(klass)
141
+ else
142
+ disabled_observers << klass
143
+ end
144
+ end
145
+
146
+ each_subclass_array do |array|
147
+ array.set_enablement(enabled, observers)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end