association_observers 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/TiagoCardoso1983/association_observers.png?branch=master)](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
|