association_observers 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,6 +9,6 @@ rvm:
9
9
  - 1.9.3
10
10
  - jruby
11
11
  - rbx
12
- script: bundle exec rspec spec
12
+ script: bundle exec rake spec
13
13
  before_script:
14
14
  - mysql -e 'create database association_observers;'
data/Gemfile CHANGED
@@ -6,9 +6,12 @@ group :development do
6
6
  end
7
7
 
8
8
  gem 'activerecord'
9
+ gem 'datamapper'
10
+ gem 'dm-mysql-adapter'
9
11
 
10
12
  platforms :ruby do
11
13
  gem 'mysql2'
14
+
12
15
  end
13
16
 
14
17
 
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
  [![Build Status](https://travis-ci.org/TiagoCardoso1983/association_observers.png?branch=master)](https://travis-ci.org/TiagoCardoso1983/association_observers)
8
-
8
+ [![Code Climate](https://codeclimate.com/badge.png)](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 only supporting ActiveRecord)
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 = ["tiago@restorm.com"]
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
@@ -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
- # 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}
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) ; base.extend(ClassMethods) ; end
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
- plural_associations = args.select{ |arg| self.reflections[arg].collection? }
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
- raise "Invalid callback; possible options: :create, :update, :save, :destroy" unless observer_callbacks.all?{|o|[:create,:update,:save,:destroy].include?(o.to_sym)}
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
- self.reflect_on_all_associations.select{ |r| args.include?(r.name) }.map{|r| [r.klass, r.options] }.each do |klass, options|
201
+ get_association_options_pairs(args).each do |klass, options|
109
202
  klass.instance_eval do
110
203
 
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
204
+ include IsObservableMethods
126
205
 
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
206
+ observable_extensions
140
207
 
141
208
  attr_reader :unobservable
142
209
 
143
- include IsObservableMethods
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.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
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
- ActiveRecord::Base.send(:extend, AssociationObservers::ClassMethods)
3
- ActiveRecord::Base.send(:include, AssociationObservers::InstanceMethods)
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.find_each(:batch_size => 10) do |observer|
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) ; observers.klass.observable? ; end
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.new_record? or not observer.respond_to?(:delay)) ? observer.send(:notify_observers, callback) : observer.delay.notify_observers(callback)
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.insert_into_active_record' do
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|
@@ -1,3 +1,3 @@
1
1
  module AssociationObservers
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -0,0 +1,5 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rubygems'
3
+ require 'active_record'
4
+ ActiveRecord::Base.configurations = YAML.load_file(File.join(File.expand_path('../..', __FILE__), 'database.yml'))
5
+ ActiveRecord::Base.establish_connection("activerecord")
@@ -1,6 +1,8 @@
1
1
  # -*- encoding : utf-8 -*-
2
+ require "./spec/active_record_helper"
2
3
  require "./spec/spec_helper"
3
4
 
5
+
4
6
  describe AssociationObservers do
5
7
  class TestUpdateNotifier < Notifier::Base
6
8
 
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+ require "./spec/active_record_helper"
2
3
  require './spec/spec_helper'
3
4
  require 'examples/readme_example'
4
5
 
@@ -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
@@ -0,0 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rubygems'
3
+ require 'data_mapper'
4
+
5
+ datamapper_config = YAML.load_file(File.join(File.expand_path('../..', __FILE__), 'database.yml'))["datamapper"]
6
+
7
+ DataMapper.setup(:default, datamapper_config)
@@ -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.3
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 00:00:00.000000000 Z
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
- description: ! "This is an alternative implementation of the observer pattern. As\
149
- \ you may know, Ruby (and Rails/ActiveRecord) already have an\n implementation\
150
- \ of it. This implementation is a variation of the pattern, so it is not supposed\
151
- \ to supersede the existing\n implementations, but \"complete\" them for the specific\
152
- \ use-cases addressed."
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
- - tiago@restorm.com
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/examples/readme_example_spec.rb
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/examples/readme_example_spec.rb
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: