association_observers 0.0.3
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.
- data/.gitignore +21 -0
- data/.pryrc +24 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +214 -0
- data/Rakefile +3 -0
- data/association_observers.gemspec +35 -0
- data/database.yml +5 -0
- data/lib/association_observers.rb +199 -0
- data/lib/association_observers/activerecord.rb +4 -0
- data/lib/association_observers/notifiers/base.rb +77 -0
- data/lib/association_observers/notifiers/propagation_notifier.rb +16 -0
- data/lib/association_observers/railtie.rb +16 -0
- data/lib/association_observers/ruby18.rb +12 -0
- data/lib/association_observers/version.rb +3 -0
- data/lib/examples/notifiers/update_timestamp_notifier.rb +29 -0
- data/lib/examples/readme_example.rb +114 -0
- data/spec/activerecord/association_observers_spec.rb +548 -0
- data/spec/examples/readme_example_spec.rb +44 -0
- data/spec/spec_helper.rb +26 -0
- metadata +223 -0
data/.gitignore
ADDED
data/.pryrc
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Pry.config.prompt = proc do |obj, level, _|
|
2
|
+
prompt = ""
|
3
|
+
prompt << "#{Rails.version}@" if defined?(Rails)
|
4
|
+
prompt << "sinatra@" if defined?(Sinatra)
|
5
|
+
prompt << "#{RUBY_VERSION}"
|
6
|
+
"#{prompt} (#{obj})> "
|
7
|
+
end
|
8
|
+
|
9
|
+
Pry.config.exception_handler = proc do |output, exception, _|
|
10
|
+
output.puts "\e[31m#{exception.class}: #{exception.message}"
|
11
|
+
output.puts "from #{exception.backtrace.first}\e[0m"
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
begin
|
17
|
+
require "awesome_print"
|
18
|
+
Pry.config.print = proc {|output, value| Pry::Helpers::BaseHelpers.stagger_output("=> #{value.ai}", output)}
|
19
|
+
rescue LoadError => err
|
20
|
+
warn "=> Unable to load awesome_print"
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
require "./lib/association_observers.rb"
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
group :development do
|
5
|
+
gem "yard", "0.8.2.1", :require => false
|
6
|
+
end
|
7
|
+
|
8
|
+
gem 'activerecord'
|
9
|
+
|
10
|
+
platforms :ruby do
|
11
|
+
gem 'mysql2'
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
platforms :jruby do
|
16
|
+
gem 'jruby-openssl'
|
17
|
+
gem 'activerecord-jdbc-adapter'
|
18
|
+
gem 'activerecord-jdbcsqlite3-adapter'
|
19
|
+
gem 'jdbc-mysql'
|
20
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Tiago Cardoso
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# AssociationObservers
|
2
|
+
|
3
|
+
This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
|
4
|
+
implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
|
5
|
+
implementations, but "complete" them for the specific use-cases addressed.
|
6
|
+
|
7
|
+
[](https://travis-ci.org/TiagoCardoso1983/association_observers)
|
8
|
+
|
9
|
+
|
10
|
+
## Comparison with the Observer Pattern
|
11
|
+
|
12
|
+
The Observer Pattern clearly defines two roles: the observer and the observed. The observer registers itself by the
|
13
|
+
observed. The observed decides when (for which "actions") to notify the observer. The observer knows what to do when notified.
|
14
|
+
|
15
|
+
What's the limitation? The observed has to know when and whom to notify. The observer has to know what to do. For this
|
16
|
+
logic to be implemented for two other separate entities, behaviour has to be copied from one place to the other. So, why
|
17
|
+
not delegate this information (to whom, when, behaviour) to a third role, the notifier?
|
18
|
+
|
19
|
+
## Comparison with Ruby Observable library
|
20
|
+
|
21
|
+
Great library, which works great for POROs, but not for models (specifically ActiveRecord, which overwrites a lot of its
|
22
|
+
functionality)
|
23
|
+
|
24
|
+
## Comparison with ActiveRecord Observers
|
25
|
+
|
26
|
+
Observers there are external entities which observe models. They don't exactly work as links between two models, just
|
27
|
+
extract functionality (callbacks) which would otherwise flood the model. For that, they're great. For the rest, not really.
|
28
|
+
|
29
|
+
|
30
|
+
### Installation
|
31
|
+
|
32
|
+
Add this line to your application's Gemfile:
|
33
|
+
|
34
|
+
gem 'association_observers'
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
$ bundle
|
39
|
+
|
40
|
+
Or install it yourself as:
|
41
|
+
|
42
|
+
$ gem install association_observers
|
43
|
+
|
44
|
+
### Usage
|
45
|
+
|
46
|
+
Here is a functional example:
|
47
|
+
|
48
|
+
require 'logger'
|
49
|
+
|
50
|
+
LOGGER = Logger.new(STDOUT)
|
51
|
+
|
52
|
+
# Notifiers
|
53
|
+
class HaveSliceNotifier < Notifier::Base
|
54
|
+
|
55
|
+
def action(cake, kid)
|
56
|
+
cake.update_column(:slices, cake.slices - 1)
|
57
|
+
cake.destroy if cake.slices == 0
|
58
|
+
kid.increment(:slices).save
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class BustKidsAssNotifier < Notifier::Base
|
64
|
+
|
65
|
+
module ObserverMethods
|
66
|
+
def bust_kids_ass!
|
67
|
+
LOGGER.info("Slam!!")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module ObservableMethods
|
72
|
+
def is_for_grandpa?
|
73
|
+
true # it is always for grandpa
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def conditions(cake, mom)
|
78
|
+
cake.is_for_grandpa?
|
79
|
+
end
|
80
|
+
|
81
|
+
def action(cake, mom)
|
82
|
+
mom.bust_kids_ass!
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class TellKidHesFatNotifier < Notifier::Base
|
88
|
+
|
89
|
+
module ObservableMethods
|
90
|
+
|
91
|
+
def cry!
|
92
|
+
LOGGER.info(":'(")
|
93
|
+
end
|
94
|
+
|
95
|
+
def throw_slices_away!
|
96
|
+
update_column(:slices, 0)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def conditions(kid, mom)
|
101
|
+
kid.slices > 20
|
102
|
+
end
|
103
|
+
|
104
|
+
def action(kid, mom)
|
105
|
+
LOGGER.info("Hey Fatty, BEEFCAKE!!!!!")
|
106
|
+
kid.cry!
|
107
|
+
kid.throw_slices_away!
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# TABLES
|
114
|
+
|
115
|
+
ActiveRecord::Schema.define do
|
116
|
+
create_table :cakes, :force => true do |t|
|
117
|
+
t.integer :slices
|
118
|
+
t.integer :mom_id
|
119
|
+
end
|
120
|
+
create_table :kids, :force => true do |t|
|
121
|
+
t.integer :mom_id
|
122
|
+
t.integer :slices, :default => 0
|
123
|
+
end
|
124
|
+
create_table :moms, :force => true do |t|
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# ENTITIES
|
129
|
+
|
130
|
+
class Cake < ActiveRecord::Base
|
131
|
+
|
132
|
+
def self.default_slices ; 8 ; end
|
133
|
+
belongs_to :mom
|
134
|
+
has_one :kid, :through => :mom
|
135
|
+
before_create do |record|
|
136
|
+
record.slices ||= record.class.default_slices
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Mom < ActiveRecord::Base
|
141
|
+
has_one :kid
|
142
|
+
has_many :cakes
|
143
|
+
end
|
144
|
+
|
145
|
+
class Kid < ActiveRecord::Base
|
146
|
+
belongs_to :mom
|
147
|
+
has_many :cakes, :through => :mom
|
148
|
+
|
149
|
+
observes :cakes, :notifier => :have_slice, :on => :create
|
150
|
+
end
|
151
|
+
|
152
|
+
class Mom < ActiveRecord::Base
|
153
|
+
observes :cakes, :on => :destroy, :notifier => :bust_kids_ass
|
154
|
+
observes :kid, :on => :update, :notifier => :tell_kid_hes_fat
|
155
|
+
end
|
156
|
+
|
157
|
+
You can find this under the examples.
|
158
|
+
|
159
|
+
The #observes method for the models accepts as argument the association being observed and a set of options:
|
160
|
+
|
161
|
+
* as : if the association is polymorphic, then this parameter has to be filled with the name by which the observer is recognized by the observed
|
162
|
+
* on : accepts one event or a set of events to observe (:create, :update, :save, :destroy) for the observed association (default: :save)
|
163
|
+
* notifier(s?): accepts one notifier or a set of notifiers that will handle the events; notifier name has to match the name of the notifier being defined by yourself;
|
164
|
+
if you don't set any notifier, then the events on the observed will only propagate to the observers of the observer (if it is being observed)
|
165
|
+
|
166
|
+
|
167
|
+
The other important task is to define your own notifiers. First, where. For Rails, the gem expects a "notifiers" folder to exist under the "app" folder.
|
168
|
+
Everywhere else, it's entirely up to you where you should define them.
|
169
|
+
Second, how. Your notifier must inherit from Notifier::Base. This class provides you with an API you should define your way:
|
170
|
+
|
171
|
+
Methods to overwrite:
|
172
|
+
* action(observable, observer) : where you should define the behaviour which results of the observation of an event
|
173
|
+
* conditions(observable, observer) : checks whether the action should be run (defaults to true if not overwritten)
|
174
|
+
|
175
|
+
Additionally, you can optimize the behaviour for collection associations. Let's say a brand has many products which know
|
176
|
+
its owner, and when the brand changes from owner, you want to update a certain flag on products. Per default, the action
|
177
|
+
will be run individually for every product. If we are talking about DB statements and 100 products, it will be 100 sequential
|
178
|
+
statements... That's a drag if you can accomplish that in one DB statement. for that, you can overwrite these two methods:
|
179
|
+
|
180
|
+
* notify_many(observable, observers)
|
181
|
+
* conditions_many(observable, observers)
|
182
|
+
|
183
|
+
the observers parameters is a container of not-yet loaded collection associations. Check your ORM's documentation to know what you can
|
184
|
+
do with it and whether you can achieve your result without populating it (there is such a use under the examples).
|
185
|
+
|
186
|
+
Purpose of the Notifier is to abstract the behaviour from the Observer relationship between associations. But if you still
|
187
|
+
need to complement/overwrite behaviour from your observer/observable models, you can write it in notifier-specific modules,
|
188
|
+
the ObserverMethods and the ObservableMethods, which will be included in the respective models.
|
189
|
+
|
190
|
+
### TODOs
|
191
|
+
|
192
|
+
* Support for other ORM's (currently only supporting ActiveRecord)
|
193
|
+
* Support for other Message Queue libraries (only supporting DelayedJob, rescue, everything that "#delay"s)
|
194
|
+
* Action routine definition on the "#observes" declaration (sometimes one does not need the overhead of writing a notifier)
|
195
|
+
* Observe method calls (currently only observing model callbacks)
|
196
|
+
* Overall spec readability
|
197
|
+
|
198
|
+
### Rails
|
199
|
+
|
200
|
+
The observer models have to be eager-loaded for the observer/observable behaviour to be extended in the respective associations.
|
201
|
+
It is kind of a drag, but a drag the Rails Observers already suffer from (these have to be declared in the application configuration).
|
202
|
+
|
203
|
+
### Non-Rails
|
204
|
+
|
205
|
+
If you are auto-loading your models, the same logic from the paragraph above applies. If you are requiring your models,
|
206
|
+
just proceed, this is not your concern :)
|
207
|
+
|
208
|
+
### Contributing
|
209
|
+
|
210
|
+
1. Fork it
|
211
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
212
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
213
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
214
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'association_observers/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "association_observers"
|
8
|
+
gem.version = AssociationObservers::VERSION
|
9
|
+
gem.authors = ["Tiago Cardoso"]
|
10
|
+
gem.email = ["tiago@restorm.com"]
|
11
|
+
gem.description = %q{This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
|
12
|
+
implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
|
13
|
+
implementations, but "complete" them for the specific use-cases addressed.}
|
14
|
+
gem.summary = %q{The Observer Pattern clearly defines two roles: the observer and the observed. The observer registers itself by the
|
15
|
+
observed. The observed decides when (for which "actions") to notify the observer. The observer knows what to do when notified.
|
16
|
+
|
17
|
+
What's the limitation? The observed has to know when and whom to notify. The observer has to know what to do. For this
|
18
|
+
logic to be implemented for two other separate entities, behaviour has to be copied from one place to the other. So, why
|
19
|
+
not delegate this information (to whom, when, behaviour) to a third role, the notifier?}
|
20
|
+
gem.homepage = "https://github.com/TiagoCardoso1983/association_observers"
|
21
|
+
|
22
|
+
gem.files = `git ls-files`.split($/)
|
23
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
24
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
25
|
+
gem.require_paths = ["lib"]
|
26
|
+
|
27
|
+
gem.add_development_dependency("rake",["~> 0.9.2.2"])
|
28
|
+
gem.add_development_dependency("rack-test",["=0.6.2"])
|
29
|
+
gem.add_development_dependency("rspec",["~> 2.11.0"])
|
30
|
+
gem.add_development_dependency("database_cleaner",["=0.8.0"])
|
31
|
+
gem.add_development_dependency("colorize",["=0.5.8"])
|
32
|
+
gem.add_development_dependency("pry")
|
33
|
+
gem.add_development_dependency("pry-doc")
|
34
|
+
gem.add_development_dependency("awesome_print")
|
35
|
+
end
|
data/database.yml
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "association_observers/version"
|
3
|
+
require "association_observers/notifiers/base"
|
4
|
+
require "association_observers/notifiers/propagation_notifier"
|
5
|
+
|
6
|
+
require "association_observers/ruby18" if RUBY_VERSION < "1.9"
|
7
|
+
|
8
|
+
# Here it is defined the basic behaviour of how observer/observable model associations are set. There are here three
|
9
|
+
# main roles defined: The observer associations, the observable associations, and the notifiers (the real observers).
|
10
|
+
# Observer Associations: those are the associations of an observable which will be "listening/observing" to updates
|
11
|
+
#
|
12
|
+
# Observable Associations: those are the associations of an observer which will trigger "listening/observing" events on these
|
13
|
+
#
|
14
|
+
# Notifiers: These are the handlers which will implement the behaviour desired, knowing who observes and who is observed
|
15
|
+
#
|
16
|
+
# Purpose of these role definitions is to separate the listening/observing behaviour from the implementation of the logic
|
17
|
+
# associated to it. Examples of these: model A has many Bs. The Bs from A are so many that you'd like to want to store
|
18
|
+
# a count of the Bs on A. For that you would like A to be informed each time a B is added/deleted, so you could update
|
19
|
+
# the counter accordingly. So in this case, A observes B, and B is observed by A. But The CounterNotifier, which is an
|
20
|
+
# entity independent from A and B, will implement the logic of updating the counter from Bs on A. This way we can achieve
|
21
|
+
# multiple behaviour implementation.
|
22
|
+
#
|
23
|
+
# @author Tiago Cardoso
|
24
|
+
module AssociationObservers
|
25
|
+
# translation of AR callbacks to collection callbacks; we want to ignore the update on collections because neither
|
26
|
+
# add nor remove shall be considered an update event in the observables
|
27
|
+
# @example
|
28
|
+
# class RichMan < ActiveRecord::Base
|
29
|
+
# has_many :cars
|
30
|
+
# observes :cars, :on => :update
|
31
|
+
# ...
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# in this example, for instance, the rich man wants only to be notified when the cars are update, not when he
|
35
|
+
# gets a new one or when one of them goes to the dumpster
|
36
|
+
COLLECTION_CALLBACKS_MAPPER = {:create => :add, :save => :add, :destroy => :remove}
|
37
|
+
|
38
|
+
# Methods to be added to observer associations
|
39
|
+
module IsObserverMethods
|
40
|
+
def self.included(base) ; base.extend(ClassMethods) ; end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def observer? ; true ; end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Methods to be added to observable associations
|
48
|
+
module IsObservableMethods
|
49
|
+
def self.included(base) ; base.extend(ClassMethods) ; end
|
50
|
+
|
51
|
+
module ClassMethods
|
52
|
+
def observable? ; true ; end
|
53
|
+
end
|
54
|
+
|
55
|
+
def unobservable! ; @unobservable = true ; end
|
56
|
+
def observable! ; @unobservable = false ; end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# informs the observers that something happened on this observable, passing all the observers to it
|
61
|
+
# @param [Symbol] callback key of the callback being notified; only the observers for this callback will be run
|
62
|
+
def notify! callback
|
63
|
+
notify_observers(callback) unless @unobservable
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module InstanceMethods
|
68
|
+
def observer? ; self.class.observer? ; end
|
69
|
+
def observable? ; self.class.observable? ; end
|
70
|
+
end
|
71
|
+
|
72
|
+
module ClassMethods
|
73
|
+
def observer? ; false ; end
|
74
|
+
def observable? ; false ; end
|
75
|
+
|
76
|
+
# DSL method which triggers all the behaviour. It sets self as an observer association from defined observable
|
77
|
+
# associations in it (these have to be defined in the model).
|
78
|
+
#
|
79
|
+
# @param [Symbol [, Symbol...]] args the self associations which will be observed
|
80
|
+
# @param [Hash] opts additional options
|
81
|
+
# @option opts [Symbol] :as name of the polymorphic association on the observables if it is defined like that
|
82
|
+
# @option opts [Symbol, Array] :observers name of the observers to be applied (in underscore notation: EmailNotifier would be :email_notifier)
|
83
|
+
# @option opts [Symbol, Array] :on which callbacks should the observers be aware of (options are :create, :save and :destroy; callback triggered will always be an after_)
|
84
|
+
def observes(*args)
|
85
|
+
opts = args.extract_options!
|
86
|
+
observer_class = self
|
87
|
+
|
88
|
+
plural_associations = args.select{ |arg| self.reflections[arg].collection? }
|
89
|
+
|
90
|
+
association_name = (opts[:as] || self.name.demodulize.underscore).to_s
|
91
|
+
notifier_classes = Array(opts[:notifiers] || opts[:notifier]).map{|notifier| notifier.to_s.end_with?("_notifier") ? notifier : "#{notifier}_notifier".to_s }
|
92
|
+
observer_callbacks = Array(opts[:on] || [:save, :destroy])
|
93
|
+
|
94
|
+
# no observer, how are you supposed to observe?
|
95
|
+
raise "Invalid callback; possible options: :create, :update, :save, :destroy" unless observer_callbacks.all?{|o|[:create,:update,:save,:destroy].include?(o.to_sym)}
|
96
|
+
|
97
|
+
# standard observer association methods
|
98
|
+
include IsObserverMethods
|
99
|
+
|
100
|
+
notifier_classes.map!{|notifier_class|notifier_class.to_s.classify.constantize} << PropagationNotifier
|
101
|
+
|
102
|
+
# observer association methods per observer
|
103
|
+
notifier_classes.each do |notifier_class|
|
104
|
+
include "#{notifier_class.name}::ObserverMethods".constantize if notifier_class.constants.map(&:to_sym).include?(:ObserverMethods)
|
105
|
+
end
|
106
|
+
|
107
|
+
# 1: for each observed association, define behaviour
|
108
|
+
self.reflect_on_all_associations.select{ |r| args.include?(r.name) }.map{|r| [r.klass, r.options] }.each do |klass, options|
|
109
|
+
klass.instance_eval do
|
110
|
+
|
111
|
+
include ActiveModel::Observing
|
112
|
+
|
113
|
+
# load observers from this observable association
|
114
|
+
observer_association_name = (options[:as] || association_name).to_s
|
115
|
+
notifier_classes.each do |notifier_class|
|
116
|
+
observer_callbacks.each do |callback|
|
117
|
+
options = {}
|
118
|
+
observer_association = self.reflect_on_association(observer_association_name.to_sym) ||
|
119
|
+
self.reflect_on_association(observer_association_name.pluralize.to_sym)
|
120
|
+
options[:observer_class] = observer_class.base_class if observer_association.options[:polymorphic]
|
121
|
+
|
122
|
+
self.add_observer notifier_class.new(callback, observer_association.name, options)
|
123
|
+
include "#{notifier_class.name}::ObservableMethods".constantize if notifier_class.constants.map(&:to_sym).include?(:ObservableMethods)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# sets the callbacks to inform observers
|
128
|
+
observer_callbacks.each do |callback|
|
129
|
+
if [:create, :update].include?(callback)
|
130
|
+
real_callback = :save
|
131
|
+
callback_opts = {:on => callback}
|
132
|
+
else
|
133
|
+
real_callback = callback
|
134
|
+
callback_opts = {}
|
135
|
+
end
|
136
|
+
send("after_#{real_callback}", callback_opts) do
|
137
|
+
notify! callback
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
attr_reader :unobservable
|
142
|
+
|
143
|
+
include IsObservableMethods
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
# 2. for each collection association, insert after add and after remove callbacks
|
149
|
+
|
150
|
+
# first step is defining the methods which will be called by the collection callbacks.
|
151
|
+
callback_procs = observer_callbacks.map do |callback|
|
152
|
+
notifier_classes.map do |notifier_class|
|
153
|
+
routine_name = :"__observer_#{callback}_callback_for_#{notifier_class.name.demodulize.underscore}__"
|
154
|
+
class_eval <<-END
|
155
|
+
def #{routine_name}(element)
|
156
|
+
callback = element.class.observer_instances.detect do |notifier|
|
157
|
+
notifier.class.name == '#{notifier_class}' and notifier.callback == :#{callback}
|
158
|
+
end
|
159
|
+
callback.notify(element, [self]) unless callback.nil?
|
160
|
+
end
|
161
|
+
private :#{routine_name}
|
162
|
+
END
|
163
|
+
[callback, routine_name]
|
164
|
+
end
|
165
|
+
end.flatten(1)
|
166
|
+
|
167
|
+
# second step is redefining the associations with the proper callbacks to be triggered
|
168
|
+
plural_associations.each do |assoc|
|
169
|
+
a = self.reflect_on_association(assoc)
|
170
|
+
callbacks = Hash[callback_procs.group_by{|code, proc| COLLECTION_CALLBACKS_MAPPER[code] }.reject{|k, v| k.nil? }.map{ |code, val| [:"after_#{code}", val.map(&:last)] }]
|
171
|
+
next if callbacks.empty?
|
172
|
+
# this snippet takes care that the array of callbacks which will get inserted in the association will not
|
173
|
+
# overwrite whatever callbacks you may have already defined
|
174
|
+
callbacks.each do |callback, procs|
|
175
|
+
callbacks[callback] += Array(a.options[callback])
|
176
|
+
end
|
177
|
+
|
178
|
+
if RUBY_VERSION < "1.9"
|
179
|
+
assoc_options = AssociationObservers::extended_to_s(a.options)
|
180
|
+
callback_options = AssociationObservers::extended_to_s(callbacks)
|
181
|
+
else
|
182
|
+
assoc_options = a.options.to_s
|
183
|
+
callback_options = callbacks
|
184
|
+
end
|
185
|
+
|
186
|
+
class_eval <<-END
|
187
|
+
#{a.macro} :#{assoc}, #{assoc_options}.merge(#{callback_options})
|
188
|
+
END
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
if defined?(Rails::Railtie) # RAILS
|
196
|
+
require 'association_observers/railtie'
|
197
|
+
else
|
198
|
+
require 'association_observers/activerecord'
|
199
|
+
end
|