association_observers 0.0.3 → 0.0.4
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/.travis.yml +1 -1
- data/Gemfile +3 -0
- data/README.md +14 -4
- data/Rakefile +20 -1
- data/association_observers.gemspec +3 -1
- data/database.yml +10 -1
- data/lib/association_observers.rb +121 -86
- data/lib/association_observers/activerecord.rb +133 -2
- data/lib/association_observers/datamapper.rb +113 -0
- data/lib/association_observers/notifiers/base.rb +1 -1
- data/lib/association_observers/notifiers/propagation_notifier.rb +2 -2
- data/lib/association_observers/railtie.rb +4 -1
- data/lib/association_observers/version.rb +1 -1
- data/spec/active_record_helper.rb +5 -0
- data/spec/activerecord/association_observers_spec.rb +2 -0
- data/spec/{examples → activerecord}/readme_example_spec.rb +1 -0
- data/spec/datamapper/association_observers_spec.rb +387 -0
- data/spec/datamapper_helper.rb +7 -0
- data/spec/spec_helper.rb +0 -6
- metadata +34 -10
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# AssociationObservers
|
2
2
|
|
3
|
-
This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
|
3
|
+
This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord, and DataMapper) already have an
|
4
4
|
implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
|
5
5
|
implementations, but "complete" them for the specific use-cases addressed.
|
6
6
|
|
7
7
|
[](https://travis-ci.org/TiagoCardoso1983/association_observers)
|
8
|
-
|
8
|
+
[](https://codeclimate.com/github/TiagoCardoso1983/association_observers)
|
9
9
|
|
10
10
|
## Comparison with the Observer Pattern
|
11
11
|
|
@@ -26,6 +26,9 @@ functionality)
|
|
26
26
|
Observers there are external entities which observe models. They don't exactly work as links between two models, just
|
27
27
|
extract functionality (callbacks) which would otherwise flood the model. For that, they're great. For the rest, not really.
|
28
28
|
|
29
|
+
## Comparison with DataMapper Observers
|
30
|
+
|
31
|
+
Currently doesn't support callbacks on collections (even though it supports observation for any method, cool!)
|
29
32
|
|
30
33
|
### Installation
|
31
34
|
|
@@ -189,12 +192,19 @@ the ObserverMethods and the ObservableMethods, which will be included in the res
|
|
189
192
|
|
190
193
|
### TODOs
|
191
194
|
|
192
|
-
* Support for other ORM's (currently
|
195
|
+
* Support for other ORM's (currently supporting ActiveRecord and DataMapper)
|
193
196
|
* Support for other Message Queue libraries (only supporting DelayedJob, rescue, everything that "#delay"s)
|
194
197
|
* 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
198
|
* Overall spec readability
|
197
199
|
|
200
|
+
* ActiveRecord: Observe method calls (currently only observing model callbacks)
|
201
|
+
* DataMapper: Support Many-to-Many collection observation (currently ignoring)
|
202
|
+
|
203
|
+
## STATUS
|
204
|
+
|
205
|
+
* Support for ActiveRecord
|
206
|
+
* Support for DataMapper
|
207
|
+
|
198
208
|
### Rails
|
199
209
|
|
200
210
|
The observer models have to be eager-loaded for the observer/observable behaviour to be extended in the respective associations.
|
data/Rakefile
CHANGED
@@ -1,3 +1,22 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
|
3
|
-
task :default do ;; end
|
3
|
+
task :default do ;; end
|
4
|
+
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:active_record_spec) do |t|
|
9
|
+
t.rspec_opts = ['--options', "\"./.rspec\""]
|
10
|
+
t.pattern = "spec/activerecord/*_spec.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec::Core::RakeTask.new(:data_mapper_spec) do |t|
|
14
|
+
t.rspec_opts = ['--options', "\"./.rspec\""]
|
15
|
+
t.pattern = "spec/datamapper/*_spec.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :spec do |t|
|
19
|
+
Rake::Task["active_record_spec"].invoke rescue (failed = true)
|
20
|
+
Rake::Task["data_mapper_spec"].invoke rescue (failed = true)
|
21
|
+
raise "failed" if failed
|
22
|
+
end
|
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
|
|
7
7
|
gem.name = "association_observers"
|
8
8
|
gem.version = AssociationObservers::VERSION
|
9
9
|
gem.authors = ["Tiago Cardoso"]
|
10
|
-
gem.email = ["
|
10
|
+
gem.email = ["cardoso_tiago@hotmail.com"]
|
11
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
12
|
implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
|
13
13
|
implementations, but "complete" them for the specific use-cases addressed.}
|
@@ -32,4 +32,6 @@ Gem::Specification.new do |gem|
|
|
32
32
|
gem.add_development_dependency("pry")
|
33
33
|
gem.add_development_dependency("pry-doc")
|
34
34
|
gem.add_development_dependency("awesome_print")
|
35
|
+
|
36
|
+
gem.add_dependency("activesupport")
|
35
37
|
end
|
data/database.yml
CHANGED
@@ -2,4 +2,13 @@ activerecord:
|
|
2
2
|
adapter: mysql2
|
3
3
|
encoding: utf8
|
4
4
|
username: root
|
5
|
-
database: association_observers
|
5
|
+
database: association_observers
|
6
|
+
|
7
|
+
|
8
|
+
datamapper:
|
9
|
+
adapter: mysql
|
10
|
+
user: root
|
11
|
+
password:
|
12
|
+
database: association_observers
|
13
|
+
host: 127.0.0.1
|
14
|
+
port: 3306
|
@@ -4,6 +4,8 @@ require "association_observers/notifiers/base"
|
|
4
4
|
require "association_observers/notifiers/propagation_notifier"
|
5
5
|
|
6
6
|
require "association_observers/ruby18" if RUBY_VERSION < "1.9"
|
7
|
+
require "active_support/core_ext/array/extract_options"
|
8
|
+
require "active_support/core_ext/string/inflections"
|
7
9
|
|
8
10
|
# Here it is defined the basic behaviour of how observer/observable model associations are set. There are here three
|
9
11
|
# main roles defined: The observer associations, the observable associations, and the notifiers (the real observers).
|
@@ -22,18 +24,41 @@ require "association_observers/ruby18" if RUBY_VERSION < "1.9"
|
|
22
24
|
#
|
23
25
|
# @author Tiago Cardoso
|
24
26
|
module AssociationObservers
|
25
|
-
|
26
|
-
#
|
27
|
-
# @
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
|
28
|
+
# @abstract
|
29
|
+
# @return [Symbol] ORM instance method name which checks whether the record is a new instance
|
30
|
+
def self.check_new_record_method
|
31
|
+
raise "should be defined in an adapter for the used ORM"
|
32
|
+
end
|
33
|
+
|
34
|
+
# @abstract
|
35
|
+
# @return [Symbol] ORM collection method name to get the model of its children
|
36
|
+
def self.fetch_model_from_collection
|
37
|
+
raise "should be defined in an adapter for the used ORM"
|
38
|
+
end
|
39
|
+
|
40
|
+
# @abstract
|
41
|
+
# implementation of an ORM-specifc batched each enumerator on a collection
|
42
|
+
def self.batched_each(collection, batch, &block)
|
43
|
+
raise "should be defined in an adapter for the used ORM"
|
44
|
+
end
|
45
|
+
|
46
|
+
# @abstract
|
47
|
+
# checks the parameters received by the observer DSL call, handles unexpected input according by triggering exceptions,
|
48
|
+
# warnings, deprecation messages
|
49
|
+
# @param [Class] observer the observer class
|
50
|
+
# @param [Array] observable_associations collection of the names of associations on the observer which will be observed
|
51
|
+
# @param [Array] notifier_classes collection of the notifiers for the observation
|
52
|
+
# @param [Array] observer_callbacks collection of the callbacks/methods to be observed
|
53
|
+
def self.validate_parameters(observer, observable_associations, notifier_classes, observer_callbacks)
|
54
|
+
raise "should be defined in an adapter for the used ORM"
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def self.included(model)
|
59
|
+
model.extend ClassMethods
|
60
|
+
model.send :include, InstanceMethods
|
61
|
+
end
|
37
62
|
|
38
63
|
# Methods to be added to observer associations
|
39
64
|
module IsObserverMethods
|
@@ -41,15 +66,77 @@ module AssociationObservers
|
|
41
66
|
|
42
67
|
module ClassMethods
|
43
68
|
def observer? ; true ; end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# @abstract
|
73
|
+
# includes modules in the observer model
|
74
|
+
def observer_extensions ; ; end
|
75
|
+
|
76
|
+
|
77
|
+
# @param [Array] association_names collection of association names
|
78
|
+
# @return [Array] a collection of association class/options pairs
|
79
|
+
def get_association_options_pairs(association_names)
|
80
|
+
raise "should be defined in an adapter for the used ORM"
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [Array] associations collection of association names
|
84
|
+
# @return [Array] the collection of associations which match collection associations
|
85
|
+
def filter_collection_associations(associations)
|
86
|
+
raise "should be defined in an adapter for the used ORM"
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# given a collection of callbacks, it defines a private routine for each of them; this subroutine will notify the observers
|
91
|
+
# which look on the callback the subroutine it is addressed to
|
92
|
+
# @param [Array] callbacks a collection of callbacks as understood by the observers (:create, :update, :update, :destroy)
|
93
|
+
# @param [Array] notifiers a collection of notifier classes
|
94
|
+
# @return [Array] a collection of callback/sob-routine pairs
|
95
|
+
def define_collection_callback_routines(callbacks, notifiers)
|
96
|
+
raise "should be defined in an adapter for the used ORM"
|
97
|
+
end
|
98
|
+
|
99
|
+
# given a set of collection associations and a collection of callback/method_name pairs, it redefines the association
|
100
|
+
# setting the respective callback routines for each of them
|
101
|
+
# @param [Array] associations a collection of plural association names
|
102
|
+
# @param [Hash, Array] callback_procs a collection of callback/method_name pairs
|
103
|
+
def redefine_collection_associations_with_collection_callbacks(associations, callback_procs)
|
104
|
+
raise "should be defined in an adapter for the used ORM"
|
105
|
+
end
|
44
106
|
end
|
45
107
|
end
|
46
108
|
|
47
109
|
# Methods to be added to observable associations
|
48
110
|
module IsObservableMethods
|
49
|
-
def self.included(base)
|
111
|
+
def self.included(base)
|
112
|
+
base.extend(ClassMethods)
|
113
|
+
end
|
50
114
|
|
51
115
|
module ClassMethods
|
52
116
|
def observable? ; true ; end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# @abstract
|
121
|
+
# includes modules in the observable model
|
122
|
+
def observable_extensions ; ; end
|
123
|
+
|
124
|
+
# @abstract
|
125
|
+
# loads the notifiers and observers for this observable class
|
126
|
+
# @param [Array] notifiers notifiers to be included
|
127
|
+
# @param [Array] callbacks collection of callbacks/methods to be observed
|
128
|
+
# @param [Class] observer_class class of the observer
|
129
|
+
# @param [Symbol] association_name name by which this observable is known in the observer
|
130
|
+
def set_observers(notifiers, callbacks, observer_class, association_name)
|
131
|
+
raise "should be defined in an adapter for the used ORM"
|
132
|
+
end
|
133
|
+
|
134
|
+
# @abstract
|
135
|
+
# sets the triggering by callbacks of the notification
|
136
|
+
# @param [Array] callbacks callbacks which will be observed and trigger notification behaviour
|
137
|
+
def set_notification_on_callbacks(callbacks)
|
138
|
+
raise "should be defined in an adapter for the used ORM"
|
139
|
+
end
|
53
140
|
end
|
54
141
|
|
55
142
|
def unobservable! ; @unobservable = true ; end
|
@@ -85,17 +172,23 @@ module AssociationObservers
|
|
85
172
|
opts = args.extract_options!
|
86
173
|
observer_class = self
|
87
174
|
|
88
|
-
|
175
|
+
|
176
|
+
# standard observer association methods
|
177
|
+
include IsObserverMethods
|
178
|
+
|
179
|
+
observer_extensions
|
180
|
+
|
181
|
+
plural_associations = filter_collection_associations(args)
|
89
182
|
|
90
183
|
association_name = (opts[:as] || self.name.demodulize.underscore).to_s
|
91
184
|
notifier_classes = Array(opts[:notifiers] || opts[:notifier]).map{|notifier| notifier.to_s.end_with?("_notifier") ? notifier : "#{notifier}_notifier".to_s }
|
92
185
|
observer_callbacks = Array(opts[:on] || [:save, :destroy])
|
93
186
|
|
94
187
|
# no observer, how are you supposed to observe?
|
95
|
-
|
188
|
+
AssociationObservers::validate_parameters(self, args, notifier_classes, observer_callbacks)
|
189
|
+
|
190
|
+
|
96
191
|
|
97
|
-
# standard observer association methods
|
98
|
-
include IsObserverMethods
|
99
192
|
|
100
193
|
notifier_classes.map!{|notifier_class|notifier_class.to_s.classify.constantize} << PropagationNotifier
|
101
194
|
|
@@ -105,42 +198,20 @@ module AssociationObservers
|
|
105
198
|
end
|
106
199
|
|
107
200
|
# 1: for each observed association, define behaviour
|
108
|
-
|
201
|
+
get_association_options_pairs(args).each do |klass, options|
|
109
202
|
klass.instance_eval do
|
110
203
|
|
111
|
-
include
|
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
|
204
|
+
include IsObservableMethods
|
126
205
|
|
127
|
-
|
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
|
206
|
+
observable_extensions
|
140
207
|
|
141
208
|
attr_reader :unobservable
|
142
209
|
|
143
|
-
|
210
|
+
# load observers from this observable association
|
211
|
+
set_observers(notifier_classes, observer_callbacks, observer_class, (options[:as] || association_name).to_s)
|
212
|
+
|
213
|
+
# sets the callbacks to inform observers
|
214
|
+
set_notification_on_callbacks(observer_callbacks)
|
144
215
|
end
|
145
216
|
|
146
217
|
end
|
@@ -148,45 +219,8 @@ module AssociationObservers
|
|
148
219
|
# 2. for each collection association, insert after add and after remove callbacks
|
149
220
|
|
150
221
|
# 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
222
|
# second step is redefining the associations with the proper callbacks to be triggered
|
168
|
-
plural_associations
|
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
|
223
|
+
redefine_collection_associations_with_collection_callbacks(plural_associations, define_collection_callback_routines(observer_callbacks, notifier_classes))
|
190
224
|
|
191
225
|
end
|
192
226
|
end
|
@@ -195,5 +229,6 @@ end
|
|
195
229
|
if defined?(Rails::Railtie) # RAILS
|
196
230
|
require 'association_observers/railtie'
|
197
231
|
else
|
198
|
-
require 'association_observers/activerecord'
|
232
|
+
require 'association_observers/activerecord' if defined?(ActiveRecord)
|
233
|
+
require 'association_observers/datamapper' if defined?(DataMapper)
|
199
234
|
end
|
@@ -1,4 +1,135 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
1
2
|
if defined?(ActiveRecord)
|
2
|
-
|
3
|
-
|
3
|
+
|
4
|
+
module AssociationObservers
|
5
|
+
def self.check_new_record_method
|
6
|
+
:new_record?
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.fetch_model_from_collection
|
10
|
+
:klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.batched_each(collection, batch, &block)
|
14
|
+
collection.find_each(:batch_size => batch, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.validate_parameters(observer, observable_associations, notifier_names, callbacks)
|
18
|
+
raise "Invalid callback; possible options: :create, :update, :save, :destroy" unless callbacks.all?{|o|[:create,:update,:save,:destroy].include?(o.to_sym)}
|
19
|
+
end
|
20
|
+
|
21
|
+
# translation of AR callbacks to collection callbacks; we want to ignore the update on collections because neither
|
22
|
+
# add nor remove shall be considered an update event in the observables
|
23
|
+
# @example
|
24
|
+
# class RichMan < ActiveRecord::Base
|
25
|
+
# has_many :cars
|
26
|
+
# observes :cars, :on => :update
|
27
|
+
# ...
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# in this example, for instance, the rich man wants only to be notified when the cars are update, not when he
|
31
|
+
# gets a new one or when one of them goes to the dumpster
|
32
|
+
COLLECTION_CALLBACKS_MAPPER = {:create => :add, :save => :add, :destroy => :remove}
|
33
|
+
|
34
|
+
module IsObservableMethods
|
35
|
+
module ClassMethods
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def set_observers(notifiers, callbacks, observer_class, association_name)
|
40
|
+
notifiers.each do |notifier|
|
41
|
+
callbacks.each do |callback|
|
42
|
+
options = {}
|
43
|
+
observer_association = self.reflect_on_association(association_name.to_sym) ||
|
44
|
+
self.reflect_on_association(association_name.pluralize.to_sym)
|
45
|
+
options[:observer_class] = observer_class.base_class if observer_association.options[:polymorphic]
|
46
|
+
|
47
|
+
self.add_observer notifier.new(callback, observer_association.name, options)
|
48
|
+
include "#{notifier.name}::ObservableMethods".constantize if notifier.constants.map(&:to_sym).include?(:ObservableMethods)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_notification_on_callbacks(callbacks)
|
54
|
+
callbacks.each do |callback|
|
55
|
+
if [:create, :update].include?(callback)
|
56
|
+
real_callback = :save
|
57
|
+
callback_opts = {:on => callback}
|
58
|
+
else
|
59
|
+
real_callback = callback
|
60
|
+
callback_opts = {}
|
61
|
+
end
|
62
|
+
send("after_#{real_callback}", callback_opts) do
|
63
|
+
notify! callback
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module IsObserverMethods
|
71
|
+
|
72
|
+
module ClassMethods
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def get_association_options_pairs(association_names)
|
77
|
+
reflect_on_all_associations.select{ |r| association_names.include?(r.name) }.map{|r| [r.klass, r.options] }
|
78
|
+
end
|
79
|
+
|
80
|
+
def filter_collection_associations(associations)
|
81
|
+
associations.select{ |arg| self.reflections[arg].collection? }
|
82
|
+
end
|
83
|
+
|
84
|
+
def define_collection_callback_routines(callbacks, notifiers)
|
85
|
+
callbacks.map do |callback|
|
86
|
+
notifiers.map do |notifier|
|
87
|
+
routine_name = :"__observer_#{callback}_callback_for_#{notifier.name.demodulize.underscore}__"
|
88
|
+
class_eval <<-END
|
89
|
+
def #{routine_name}(element)
|
90
|
+
callback = element.class.observer_instances.detect do |notifier|
|
91
|
+
notifier.class.name == '#{notifier}' and notifier.callback == :#{callback}
|
92
|
+
end
|
93
|
+
callback.notify(element, [self]) unless callback.nil?
|
94
|
+
end
|
95
|
+
private :#{routine_name}
|
96
|
+
END
|
97
|
+
[callback, routine_name]
|
98
|
+
end
|
99
|
+
end.flatten(1)
|
100
|
+
end
|
101
|
+
|
102
|
+
def redefine_collection_associations_with_collection_callbacks(associations, callback_procs)
|
103
|
+
associations.each do |assoc|
|
104
|
+
a = self.reflect_on_association(assoc)
|
105
|
+
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)] }]
|
106
|
+
next if callbacks.empty? # no callbacks, no need to redefine association
|
107
|
+
|
108
|
+
# this snippet takes care that the array of callbacks which will get inserted in the association will not
|
109
|
+
# overwrite whatever callbacks you may have already defined
|
110
|
+
callbacks.each do |callback, procedures|
|
111
|
+
callbacks[callback] += Array(a.options[callback])
|
112
|
+
end
|
113
|
+
|
114
|
+
# bullshit ruby 1.8 can't stringify hashes, arrays, symbols nor strings correctly
|
115
|
+
if RUBY_VERSION < "1.9"
|
116
|
+
assoc_options = AssociationObservers::extended_to_s(a.options)
|
117
|
+
callback_options = AssociationObservers::extended_to_s(callbacks)
|
118
|
+
else
|
119
|
+
assoc_options = a.options.to_s
|
120
|
+
callback_options = callbacks
|
121
|
+
end
|
122
|
+
|
123
|
+
class_eval <<-END
|
124
|
+
#{a.macro} :#{assoc}, #{assoc_options}.merge(#{callback_options})
|
125
|
+
END
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
ActiveRecord::Base.send(:include, AssociationObservers)
|
4
135
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
if defined?(DataMapper)
|
3
|
+
module AssociationObservers
|
4
|
+
def self.check_new_record_method
|
5
|
+
:new?
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.fetch_model_from_collection
|
9
|
+
:model
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.batched_each(collection, batch, &block)
|
13
|
+
collection.each(&block) # datamapper batches already by 500 https://groups.google.com/forum/?fromgroups=#!searchin/datamapper/batches/datamapper/lAZWFN4TWAA/G1Gu-ams_QMJ
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.validate_parameters(observer, observable_associations, notifier_names, callbacks)
|
17
|
+
observable_associations.each do |o|
|
18
|
+
if observer.relationships[o].is_a?(DataMapper::Associations::ManyToMany::Relationship)
|
19
|
+
warn "this gem does not currently support observation behaviour for many to many relationships"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module IsObservableMethods
|
25
|
+
def self.included(model)
|
26
|
+
model.extend(ClassMethods)
|
27
|
+
model.send :include, InstanceMethods
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def notifiers
|
32
|
+
@notifiers ||= []
|
33
|
+
end
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_observers(ntfs, callbacks, observer_class, association_name)
|
37
|
+
ntfs.each do |notifier|
|
38
|
+
callbacks.each do |callback|
|
39
|
+
options = {} # todo: use this for polymorphics
|
40
|
+
observer_association = self.relationships[association_name]||
|
41
|
+
self.relationships[association_name.pluralize]
|
42
|
+
notifiers << notifier.new(callback, observer_association.name, options)
|
43
|
+
include "#{notifier.name}::ObservableMethods".constantize if notifier.constants.map(&:to_sym).include?(:ObservableMethods)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_notification_on_callbacks(callbacks)
|
49
|
+
callbacks.each do |callback|
|
50
|
+
after callback do
|
51
|
+
notify! callback
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
|
64
|
+
def notify_observers(callback)
|
65
|
+
self.class.notifiers.each{|notifier| notifier.update(callback, self)}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module IsObserverMethods
|
71
|
+
|
72
|
+
module ClassMethods
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def observer_extensions
|
77
|
+
#include DataMapper::Observer
|
78
|
+
end
|
79
|
+
|
80
|
+
def get_association_options_pairs(association_names)
|
81
|
+
# TODO: find better way to figure out the class of the relationship entity
|
82
|
+
relationships.select{|r|association_names.include?(r.name)}.map{|r| [(r.is_a?(DataMapper::Associations::ManyToOne::Relationship) ? r.parent_model : r.child_model), r.options] }
|
83
|
+
end
|
84
|
+
|
85
|
+
def filter_collection_associations(associations)
|
86
|
+
associations.select{ |arg| self.relationships[arg].options[:max] == Infinity }
|
87
|
+
end
|
88
|
+
|
89
|
+
def define_collection_callback_routines(callbacks, notifiers)
|
90
|
+
callbacks
|
91
|
+
end
|
92
|
+
|
93
|
+
def redefine_collection_associations_with_collection_callbacks(associations, callbacks)
|
94
|
+
associations.each do |assoc|
|
95
|
+
callbacks.each do |callback|
|
96
|
+
relationship = relationships[assoc]
|
97
|
+
model_method = relationship.is_a?(DataMapper::Associations::ManyToOne::Relationship ) ?
|
98
|
+
:parent_model :
|
99
|
+
:child_model
|
100
|
+
relationship.send(model_method).after callback do
|
101
|
+
notify! callback
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
DataMapper::Model.append_inclusions AssociationObservers
|
113
|
+
end
|
@@ -58,7 +58,7 @@ module Notifier
|
|
58
58
|
# @param [Array[ActiveRecord::Relation]] many_observers the observers which will be notified; each element represents a one-to-many relation
|
59
59
|
def notify_many(observable, many_observers)
|
60
60
|
many_observers.each do |observers|
|
61
|
-
observers
|
61
|
+
AssociationObservers::batched_each(observers, 10) do |observer|
|
62
62
|
yield(observable, observer) if conditions(observable, observer)
|
63
63
|
end if conditions_many(observable, observers)
|
64
64
|
end
|
@@ -5,12 +5,12 @@
|
|
5
5
|
class PropagationNotifier < Notifier::Base
|
6
6
|
|
7
7
|
def conditions(observable, observer) ; observer.observable? ; end
|
8
|
-
def conditions_many(observable, observers) ;
|
8
|
+
def conditions_many(observable, observers) ; observers.send(AssociationObservers::fetch_model_from_collection).observable? ; end
|
9
9
|
|
10
10
|
# propagates the message to the observer's observer if the
|
11
11
|
# observer is indeed observed by any entity
|
12
12
|
def action(observable, observer, callback=@callback)
|
13
|
-
(observer.
|
13
|
+
(observer.send(AssociationObservers::check_new_record_method) or not observer.respond_to?(:delay)) ? observer.send(:notify_observers, callback) : observer.delay.notify_observers(callback)
|
14
14
|
end
|
15
15
|
|
16
16
|
end
|
@@ -4,9 +4,12 @@ module AssociationObservers
|
|
4
4
|
ActiveSupport.on_load :active_record do
|
5
5
|
require 'association_observers/activerecord'
|
6
6
|
end
|
7
|
+
ActiveSupport.on_load :data_mapper do
|
8
|
+
require 'association_observers/datamapper'
|
9
|
+
end
|
7
10
|
end
|
8
11
|
class Railtie < Rails::Railtie
|
9
|
-
initializer 'association_observers.
|
12
|
+
initializer 'association_observers.insert_into_orm' do
|
10
13
|
AssociationObservers.initialize_railtie
|
11
14
|
end
|
12
15
|
initializer 'association_observers.autoload', :before => :set_autoload_paths do |app|
|
@@ -0,0 +1,387 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "./spec/datamapper_helper"
|
3
|
+
require "./spec/spec_helper"
|
4
|
+
|
5
|
+
|
6
|
+
describe AssociationObservers do
|
7
|
+
class TestUpdateNotifier < Notifier::Base
|
8
|
+
|
9
|
+
def action(observable, observer)
|
10
|
+
if observer.dirty?
|
11
|
+
observer.updated = true
|
12
|
+
else
|
13
|
+
observer.update!(:updated => true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def notify_many(observable, many_observers)
|
18
|
+
many_observers.each do |observers|
|
19
|
+
observers.update!(:updated => true)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
class TestDestroyNotifier < Notifier::Base
|
26
|
+
|
27
|
+
def action(observable, observer)
|
28
|
+
observer.update(:deleted => true)
|
29
|
+
end
|
30
|
+
|
31
|
+
def notify_many(observable, many_observers)
|
32
|
+
many_observers.each do |observers|
|
33
|
+
observers.each do |observer|
|
34
|
+
observer.update!(:deleted => true)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
class BelongsToObservableTest
|
43
|
+
include DataMapper::Resource
|
44
|
+
property :id, Serial
|
45
|
+
property :name, String
|
46
|
+
|
47
|
+
has 1, :observer_test
|
48
|
+
|
49
|
+
def bang
|
50
|
+
"Bang"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
class CollectionObservableTest
|
55
|
+
include DataMapper::Resource
|
56
|
+
property :id, Serial
|
57
|
+
property :name, String
|
58
|
+
|
59
|
+
has n, :observer_tests
|
60
|
+
end
|
61
|
+
class HasOneObservableTest
|
62
|
+
include DataMapper::Resource
|
63
|
+
property :id, Serial
|
64
|
+
property :name, String
|
65
|
+
|
66
|
+
belongs_to :observer_test, :required => false
|
67
|
+
end
|
68
|
+
class HasManyObservableTest
|
69
|
+
include DataMapper::Resource
|
70
|
+
property :id, Serial
|
71
|
+
property :name, String
|
72
|
+
|
73
|
+
belongs_to :observer_test, :required => false
|
74
|
+
has n, :has_many_through_observable_tests
|
75
|
+
end
|
76
|
+
class HasManyThroughObservableTest
|
77
|
+
include DataMapper::Resource
|
78
|
+
property :id, Serial
|
79
|
+
property :name, String
|
80
|
+
|
81
|
+
belongs_to :has_many_observable_test, :required => false
|
82
|
+
has 1, :observer_test, :through => :has_many_observable_test
|
83
|
+
end
|
84
|
+
|
85
|
+
class HabtmObservableTest
|
86
|
+
include DataMapper::Resource
|
87
|
+
property :id, Serial
|
88
|
+
property :name, String
|
89
|
+
|
90
|
+
has n, :observer_tests, :through => Resource
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
class ObserverTest
|
96
|
+
include DataMapper::Resource
|
97
|
+
|
98
|
+
property :id, Serial
|
99
|
+
property :type, Discriminator
|
100
|
+
property :updated, Boolean
|
101
|
+
property :deleted, Boolean
|
102
|
+
|
103
|
+
belongs_to :belongs_to_observable_test, :required => false
|
104
|
+
belongs_to :collection_observable_test, :required => false
|
105
|
+
has 1, :has_one_observable_test
|
106
|
+
has n, :has_many_observable_tests
|
107
|
+
|
108
|
+
has n, :has_many_through_observable_tests, :through => :has_many_observable_tests, :via => :target, :required => false
|
109
|
+
|
110
|
+
|
111
|
+
has 1, :observer_observer_test
|
112
|
+
|
113
|
+
has n, :habtm_observable_tests, :through => Resource
|
114
|
+
|
115
|
+
observes :habtm_observable_tests, :notifiers => :test_update, :on => :create
|
116
|
+
observes :belongs_to_observable_test,
|
117
|
+
:has_one_observable_test,
|
118
|
+
:collection_observable_test,
|
119
|
+
:has_many_through_observable_tests,
|
120
|
+
:has_many_observable_tests,
|
121
|
+
:habtm_observable_tests, :notifiers => :test_update, :on => :update
|
122
|
+
observes :belongs_to_observable_test,
|
123
|
+
:has_one_observable_test,
|
124
|
+
:collection_observable_test,
|
125
|
+
:has_many_through_observable_tests,
|
126
|
+
:has_many_observable_tests,
|
127
|
+
:habtm_observable_tests, :notifiers => :test_destroy, :on => :destroy
|
128
|
+
observes :belongs_to_observable_test, :notifiers => :test_update, :on => :bang
|
129
|
+
end
|
130
|
+
|
131
|
+
class ObserverObserverTest
|
132
|
+
include DataMapper::Resource
|
133
|
+
|
134
|
+
property :id, Serial
|
135
|
+
property :type, Discriminator
|
136
|
+
property :updated, Boolean
|
137
|
+
property :deleted, Boolean
|
138
|
+
|
139
|
+
belongs_to :observer_test, :required => false
|
140
|
+
|
141
|
+
observes :observer_test, :notifiers => :test_update, :on => :update
|
142
|
+
end
|
143
|
+
|
144
|
+
DataMapper.finalize
|
145
|
+
|
146
|
+
DataMapper.auto_migrate!
|
147
|
+
|
148
|
+
let(:observer1) {ObserverTest.create(:has_one_observable_test => HasOneObservableTest.new,
|
149
|
+
:has_many_observable_tests => [HasManyObservableTest.new,
|
150
|
+
HasManyObservableTest.new,
|
151
|
+
HasManyObservableTest.new])}
|
152
|
+
let(:observer2) { ObserverTest.create }
|
153
|
+
let(:belongs_to_observable) {BelongsToObservableTest.create(:observer_test => observer1)}
|
154
|
+
let(:collection_observable) {CollectionObservableTest.create(:observer_tests => [observer1, observer2])}
|
155
|
+
describe "observer_methods" do
|
156
|
+
let(:observer) {ObserverObserverTest.new}
|
157
|
+
it "should be available" do
|
158
|
+
observer.should be_observer
|
159
|
+
observer.should_not be_observable
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "observable_methods" do
|
164
|
+
let(:observable){BelongsToObservableTest.new}
|
165
|
+
it "should be available" do
|
166
|
+
observable.should_not be_observer
|
167
|
+
observable.should be_observable
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "when the belongs to association gets banged" do
|
172
|
+
before(:each) do
|
173
|
+
belongs_to_observable.bang
|
174
|
+
end
|
175
|
+
it "should update its observer" do
|
176
|
+
observer1.reload.should be_updated
|
177
|
+
observer1.should_not be_deleted
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe "when the belongs to observable is updated" do
|
182
|
+
before(:each) do
|
183
|
+
belongs_to_observable.update(:name => "doof")
|
184
|
+
end
|
185
|
+
it "should update its observer" do
|
186
|
+
belongs_to_observable.name.should == "doof"
|
187
|
+
observer1.reload.should be_updated
|
188
|
+
observer1.should_not be_deleted
|
189
|
+
end
|
190
|
+
end
|
191
|
+
pending "when the belongs to observable is deleted" do
|
192
|
+
before(:each) do
|
193
|
+
observer1.belongs_to_observable_test.destroy
|
194
|
+
end
|
195
|
+
it "should destroy its observer" do
|
196
|
+
observer1.reload.should be_deleted
|
197
|
+
end
|
198
|
+
end
|
199
|
+
describe "when the observable hides itself" do
|
200
|
+
before(:each) do
|
201
|
+
observer1.update!(:updated => nil)
|
202
|
+
belongs_to_observable.unobservable!
|
203
|
+
belongs_to_observable.update(:name => "doof")
|
204
|
+
end
|
205
|
+
it "should not update its observer" do
|
206
|
+
observer1.belongs_to_observable_test.name.should == "doof"
|
207
|
+
observer1.reload.should_not be_updated
|
208
|
+
end
|
209
|
+
end
|
210
|
+
describe "when the has one observable is updated" do
|
211
|
+
before(:each) do
|
212
|
+
observer1.has_one_observable_test.update(:name => "doof")
|
213
|
+
end
|
214
|
+
it "should update its observer" do
|
215
|
+
observer1.has_one_observable_test.name.should == "doof"
|
216
|
+
observer1.reload.should be_updated
|
217
|
+
observer1.should_not be_deleted
|
218
|
+
end
|
219
|
+
end
|
220
|
+
describe "when one of the has many observables is updated" do
|
221
|
+
before(:each) do
|
222
|
+
observer1.has_many_observable_tests.first.update(:name => "doof")
|
223
|
+
end
|
224
|
+
it "should update its observer" do
|
225
|
+
observer1.has_many_observable_tests.first.name.should == "doof"
|
226
|
+
observer1.reload.should be_updated
|
227
|
+
observer1.should_not be_deleted
|
228
|
+
end
|
229
|
+
describe "and afterwards deleted" do
|
230
|
+
before(:each) do
|
231
|
+
observer1.update!(:deleted => false)
|
232
|
+
observer1.has_many_observable_tests.first.destroy
|
233
|
+
end
|
234
|
+
it "should update its observer" do
|
235
|
+
observer1.reload.should be_deleted
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
describe "when the has many through has been updated" do
|
240
|
+
before(:each) do
|
241
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.create
|
242
|
+
observer1.update!(:updated => false)
|
243
|
+
end
|
244
|
+
it "should update its observer" do
|
245
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.first.update(:name => "doof")
|
246
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.first.name.should == "doof"
|
247
|
+
observer1.reload.should be_updated
|
248
|
+
observer1.should_not be_deleted
|
249
|
+
end
|
250
|
+
end
|
251
|
+
describe "when the collection observable is updated" do
|
252
|
+
before(:each) do
|
253
|
+
collection_observable.update(:name => "doof")
|
254
|
+
end
|
255
|
+
it "should update its observers" do
|
256
|
+
collection_observable.name.should == "doof"
|
257
|
+
observer1.reload.should be_updated
|
258
|
+
observer1.should_not be_deleted
|
259
|
+
observer2.reload.should be_updated
|
260
|
+
observer2.should_not be_deleted
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
describe "when the observer has an observer itself" do
|
265
|
+
let(:observer_observer) { ObserverObserverTest.create }
|
266
|
+
before(:each) do
|
267
|
+
observer1.update(:observer_observer_test => observer_observer)
|
268
|
+
end
|
269
|
+
describe "when the belongs to observable is updated" do
|
270
|
+
before(:each) do
|
271
|
+
belongs_to_observable.update(:name => "doof")
|
272
|
+
end
|
273
|
+
it "should update its observer and its observer's observer" do
|
274
|
+
belongs_to_observable.name.should == "doof"
|
275
|
+
observer1.reload.should be_updated
|
276
|
+
observer_observer.reload.should be_updated
|
277
|
+
end
|
278
|
+
end
|
279
|
+
describe "when the has one observable is updated" do
|
280
|
+
before(:each) do
|
281
|
+
observer_observer.observer_test.has_one_observable_test.update(:name => "doof")
|
282
|
+
end
|
283
|
+
it "should update its observer and its observer's observer" do
|
284
|
+
observer_observer.observer_test.has_one_observable_test.name.should == "doof"
|
285
|
+
observer1.reload.should be_updated
|
286
|
+
observer_observer.reload.should be_updated
|
287
|
+
end
|
288
|
+
end
|
289
|
+
describe "when one of the has many observables is updated" do
|
290
|
+
before(:each) do
|
291
|
+
observer_observer.observer_test.has_many_observable_tests.first.update(:name => "doof")
|
292
|
+
end
|
293
|
+
it "should update its observer and its observer's observer" do
|
294
|
+
observer_observer.observer_test.has_many_observable_tests.first.name.should == "doof"
|
295
|
+
observer1.reload.should be_updated
|
296
|
+
observer_observer.reload.should be_updated
|
297
|
+
end
|
298
|
+
end
|
299
|
+
describe "when the has many through has been updated" do
|
300
|
+
before(:each) do
|
301
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.create
|
302
|
+
observer1.update!(:updated => false)
|
303
|
+
observer_observer.update!(:updated => false)
|
304
|
+
end
|
305
|
+
it "should update its observer" do
|
306
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.first.update(:name => "doof")
|
307
|
+
observer1.has_many_observable_tests.first.has_many_through_observable_tests.first.name.should == "doof"
|
308
|
+
observer1.reload.should be_updated
|
309
|
+
observer_observer.reload.should be_updated
|
310
|
+
end
|
311
|
+
end
|
312
|
+
describe "when the collection observable is updated" do
|
313
|
+
before(:each) do
|
314
|
+
collection_observable
|
315
|
+
observer_observer.observer_test.reload.collection_observable_test.update(:name => "doof")
|
316
|
+
end
|
317
|
+
it "should update its observers and its observer's observer" do
|
318
|
+
observer_observer.observer_test.collection_observable_test.name.should == "doof"
|
319
|
+
observer1.reload.should be_updated
|
320
|
+
observer_observer.reload.should be_updated
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
describe "when the association is a joined through" do
|
326
|
+
class HasOneThroughObservableTest
|
327
|
+
include DataMapper::Resource
|
328
|
+
property :id, Serial
|
329
|
+
property :name, String
|
330
|
+
|
331
|
+
belongs_to :has_one_observable_test, :required => false
|
332
|
+
has 1, :observer_test, :through => :has_one_observable_test
|
333
|
+
end
|
334
|
+
|
335
|
+
class HasOneObservableTest
|
336
|
+
has 1, :has_one_through_observable_test
|
337
|
+
end
|
338
|
+
|
339
|
+
class ObserverTest
|
340
|
+
|
341
|
+
has 1, :has_one_through_observable_test, :through => :has_one_observable_test
|
342
|
+
observes :has_one_through_observable_test, :notifiers => :test_update, :on => :update
|
343
|
+
end
|
344
|
+
|
345
|
+
DataMapper.auto_upgrade!
|
346
|
+
|
347
|
+
let(:through_observable) { HasOneThroughObservableTest.create(:has_one_observable_test => HasOneObservableTest.new(:observer_test => ObserverTest.new)) }
|
348
|
+
let(:observer) {through_observable.has_one_observable_test.observer_test}
|
349
|
+
describe "when the has one association is updated" do
|
350
|
+
before(:each) do
|
351
|
+
observer.update(:updated => nil)
|
352
|
+
through_observable.update(:name => "doof")
|
353
|
+
end
|
354
|
+
it "should update its observer" do
|
355
|
+
through_observable.name.should == "doof"
|
356
|
+
observer.reload.should be_updated
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
end
|
361
|
+
|
362
|
+
# TODO: should this be responsibility of the associations or from the observers? check further into this
|
363
|
+
pending "when an observable also observes its observer" do
|
364
|
+
|
365
|
+
class ObservableAbstractTest
|
366
|
+
property :successful, Boolean
|
367
|
+
end
|
368
|
+
|
369
|
+
DataMapper.auto_upgrade!
|
370
|
+
|
371
|
+
before(:all) do
|
372
|
+
class HasOneObservableTest
|
373
|
+
observes :observer_test, :notifiers => :test_update, :on => :update
|
374
|
+
end
|
375
|
+
end
|
376
|
+
describe "when something changes on the observable" do
|
377
|
+
before(:each) do
|
378
|
+
observer1.has_one_observable_test.update(:name => "doof")
|
379
|
+
end
|
380
|
+
it "should update only the observer (and therefore avoid infinite cycle)" do
|
381
|
+
observer1.has_one_observable_test.name.should == "doof"
|
382
|
+
observer1.reload.should be_updated
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,13 +1,7 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
require 'rubygems'
|
3
|
-
require 'active_record'
|
4
3
|
require './lib/association_observers'
|
5
4
|
|
6
|
-
|
7
|
-
ActiveRecord::Base.configurations = YAML.load_file(File.join(File.expand_path('../..', __FILE__), 'database.yml'))
|
8
|
-
ActiveRecord::Base.establish_connection("activerecord")
|
9
|
-
|
10
|
-
|
11
5
|
require 'rspec'
|
12
6
|
require 'database_cleaner'
|
13
7
|
|
metadata
CHANGED
@@ -2,14 +2,14 @@
|
|
2
2
|
name: association_observers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.4
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Tiago Cardoso
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-14 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -145,13 +145,30 @@ dependencies:
|
|
145
145
|
none: false
|
146
146
|
prerelease: false
|
147
147
|
type: :development
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
148
|
+
- !ruby/object:Gem::Dependency
|
149
|
+
name: activesupport
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - ! '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: !binary |-
|
155
|
+
MA==
|
156
|
+
none: false
|
157
|
+
requirement: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ! '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: !binary |-
|
162
|
+
MA==
|
163
|
+
none: false
|
164
|
+
prerelease: false
|
165
|
+
type: :runtime
|
166
|
+
description: |-
|
167
|
+
This is an alternative implementation of the observer pattern. As you may know, Ruby (and Rails/ActiveRecord) already have an
|
168
|
+
implementation of it. This implementation is a variation of the pattern, so it is not supposed to supersede the existing
|
169
|
+
implementations, but "complete" them for the specific use-cases addressed.
|
153
170
|
email:
|
154
|
-
-
|
171
|
+
- cardoso_tiago@hotmail.com
|
155
172
|
executables: []
|
156
173
|
extensions: []
|
157
174
|
extra_rdoc_files: []
|
@@ -168,6 +185,7 @@ files:
|
|
168
185
|
- database.yml
|
169
186
|
- lib/association_observers.rb
|
170
187
|
- lib/association_observers/activerecord.rb
|
188
|
+
- lib/association_observers/datamapper.rb
|
171
189
|
- lib/association_observers/notifiers/base.rb
|
172
190
|
- lib/association_observers/notifiers/propagation_notifier.rb
|
173
191
|
- lib/association_observers/railtie.rb
|
@@ -175,8 +193,11 @@ files:
|
|
175
193
|
- lib/association_observers/version.rb
|
176
194
|
- lib/examples/notifiers/update_timestamp_notifier.rb
|
177
195
|
- lib/examples/readme_example.rb
|
196
|
+
- spec/active_record_helper.rb
|
178
197
|
- spec/activerecord/association_observers_spec.rb
|
179
|
-
- spec/
|
198
|
+
- spec/activerecord/readme_example_spec.rb
|
199
|
+
- spec/datamapper/association_observers_spec.rb
|
200
|
+
- spec/datamapper_helper.rb
|
180
201
|
- spec/spec_helper.rb
|
181
202
|
homepage: https://github.com/TiagoCardoso1983/association_observers
|
182
203
|
licenses: []
|
@@ -217,7 +238,10 @@ summary: ! 'The Observer Pattern clearly defines two roles: the observer and the
|
|
217
238
|
behaviour has to be copied from one place to the other. So, why not delegate this
|
218
239
|
information (to whom, when, behaviour) to a third role, the notifier?'
|
219
240
|
test_files:
|
241
|
+
- spec/active_record_helper.rb
|
220
242
|
- spec/activerecord/association_observers_spec.rb
|
221
|
-
- spec/
|
243
|
+
- spec/activerecord/readme_example_spec.rb
|
244
|
+
- spec/datamapper/association_observers_spec.rb
|
245
|
+
- spec/datamapper_helper.rb
|
222
246
|
- spec/spec_helper.rb
|
223
247
|
has_rdoc:
|