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,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