association_observers 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +1 -0
  3. data/Gemfile +6 -0
  4. data/README.md +57 -2
  5. data/Rakefile +18 -0
  6. data/association_observers.gemspec +3 -3
  7. data/lib/association_observers.rb +43 -31
  8. data/lib/association_observers/{activerecord.rb → active_record.rb} +15 -16
  9. data/lib/association_observers/{datamapper.rb → data_mapper.rb} +11 -20
  10. data/lib/association_observers/extensions/delayed_job.rb +13 -0
  11. data/lib/association_observers/extensions/resque.rb +30 -0
  12. data/lib/association_observers/extensions/sidekiq.rb +32 -0
  13. data/lib/association_observers/notifiers/base.rb +46 -21
  14. data/lib/association_observers/notifiers/propagation_notifier.rb +2 -2
  15. data/lib/association_observers/orm/active_record.rb +48 -0
  16. data/lib/association_observers/orm/base.rb +61 -0
  17. data/lib/association_observers/orm/data_mapper.rb +61 -0
  18. data/lib/association_observers/queue.rb +71 -0
  19. data/lib/association_observers/railtie.rb +11 -4
  20. data/lib/association_observers/ruby18.rb +26 -8
  21. data/lib/association_observers/version.rb +1 -1
  22. data/lib/association_observers/workers/many_delayed_notification.rb +41 -0
  23. data/spec/activerecord/association_observers_spec.rb +32 -1
  24. data/spec/activerecord/delayed_job/association_observers_spec.rb +8 -0
  25. data/spec/activerecord/readme_example_spec.rb +1 -1
  26. data/spec/activerecord/resque/association_observers_spec.rb +8 -0
  27. data/spec/activerecord/sidekiq/association_observers_spec.rb +8 -0
  28. data/spec/datamapper/association_observers_spec.rb +37 -2
  29. data/spec/{active_record_helper.rb → helpers/active_record_helper.rb} +4 -1
  30. data/spec/{datamapper_helper.rb → helpers/datamapper_helper.rb} +1 -1
  31. data/spec/helpers/delayed_job_helper.rb +26 -0
  32. data/spec/helpers/resque_helper.rb +5 -0
  33. data/spec/helpers/sidekiq_helper.rb +4 -0
  34. metadata +174 -156
data/.gitignore CHANGED
@@ -15,6 +15,7 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ log/
18
19
 
19
20
  .rvmrc
20
21
  .idea
@@ -7,6 +7,7 @@ rvm:
7
7
  - 1.8.7
8
8
  - 1.9.2
9
9
  - 1.9.3
10
+ - 2.0.0
10
11
  - jruby
11
12
  - rbx
12
13
  script: bundle exec rake spec
data/Gemfile CHANGED
@@ -5,6 +5,12 @@ group :development do
5
5
  gem "yard", "0.8.2.1", :require => false
6
6
  end
7
7
 
8
+ group :test do
9
+ gem "delayed_job_active_record", :require => false
10
+ gem "resque", :require => false
11
+ gem "sidekiq", :require => false unless RUBY_VERSION == "1.8.7"
12
+ end
13
+
8
14
  gem 'activerecord'
9
15
  gem 'datamapper'
10
16
  gem 'dm-mysql-adapter'
data/README.md CHANGED
@@ -169,7 +169,7 @@ The #observes method for the models accepts as argument the association being ob
169
169
 
170
170
  The other important task is to define your own notifiers. First, where. For Rails, the gem expects a "notifiers" folder to exist under the "app" folder.
171
171
  Everywhere else, it's entirely up to you where you should define them.
172
- Second, how. Your notifier must inherit from Notifier::Base. This class provides you with an API you should define your way:
172
+ Second, how. Your notifier must inherit from Notifier::Base. This class provides you with hook methods you should/could redefine your way:
173
173
 
174
174
  Methods to overwrite:
175
175
  * action(observable, observer) : where you should define the behaviour which results of the observation of an event
@@ -190,10 +190,63 @@ Purpose of the Notifier is to abstract the behaviour from the Observer relations
190
190
  need to complement/overwrite behaviour from your observer/observable models, you can write it in notifier-specific modules,
191
191
  the ObserverMethods and the ObservableMethods, which will be included in the respective models.
192
192
 
193
+ ### Background Queues
194
+
195
+ This gem supports the 3 most popular background queue implementations around: Delayed Job, Resque and Sidekiq. If you are
196
+ using this gem out of Rails, you can do:
197
+
198
+ AssociationObservers::options[:queue][:engine] = :delayed_job # or :sidekiq, or :resque
199
+
200
+ if in Rails, you can do it in application.rb:
201
+
202
+ module YourApp
203
+ class Application < Rails::Application
204
+ ...
205
+ config.association_observers.queue.engine = :delayed_job # or :sidekiq, or :resque
206
+ ...
207
+ end
208
+ end
209
+
210
+ and that's it. Why is this important? The notification of observer collections, when you don't rewrite the #notify_many hook
211
+ method of the notifier, takes the following approach:
212
+
213
+ * Queries the collection in batches (of 50, by default)
214
+ * Iterates over each batch
215
+ * Performs the #update hook method on each
216
+
217
+ It also takes this approach to the propagation. Batch-querying is much better than querying all at once, but you still
218
+ load everything and perform the various iterations synchronously. If you already use one of the aforementioned background
219
+ queues, each batch will generate a job which will be handled asynchronously. You also have the control over the batch size.
220
+ If you want to set a new default batch size, just:
221
+
222
+ # standard
223
+ AssociationObservers::options[:batch_size] = 200 # or 20...
224
+
225
+ # rails way
226
+ module YourApp
227
+ class Application < Rails::Application
228
+ ...
229
+ config.association_observers.batch_size = 200 # or 20...
230
+ ...
231
+ end
232
+ end
233
+
234
+ You can also set the queue name (default: "observers" and priority (if using delayed job) on the options, they are just another option under queue.
235
+
236
+ ### Ruby Support
237
+
238
+ This gem is tested against the following rubies:
239
+
240
+ * 1.8.7
241
+ * 1.9.2
242
+ * 1.9.3
243
+ * 2.0.0
244
+ * rubinius
245
+ * jruby
246
+
193
247
  ### TODOs
194
248
 
195
249
  * Support for other ORM's (currently supporting ActiveRecord and DataMapper)
196
- * Support for other Message Queue libraries (only supporting DelayedJob, rescue, everything that "#delay"s)
197
250
  * Action routine definition on the "#observes" declaration (sometimes one does not need the overhead of writing a notifier)
198
251
  * Overall spec readability
199
252
 
@@ -205,6 +258,8 @@ the ObserverMethods and the ObservableMethods, which will be included in the res
205
258
  * Support for ActiveRecord
206
259
  * Support for DataMapper
207
260
 
261
+ * Integration with delayed job, resque and sidekiq
262
+
208
263
  ### Rails
209
264
 
210
265
  The observer models have to be eager-loaded for the observer/observable behaviour to be extended in the respective associations.
data/Rakefile CHANGED
@@ -10,6 +10,21 @@ RSpec::Core::RakeTask.new(:active_record_spec) do |t|
10
10
  t.pattern = "spec/activerecord/*_spec.rb"
11
11
  end
12
12
 
13
+ RSpec::Core::RakeTask.new(:active_record_delayed_job_spec) do |t|
14
+ t.rspec_opts = ['--options', "\"./.rspec\""]
15
+ t.pattern = "spec/activerecord/delayed_job/*_spec.rb"
16
+ end
17
+
18
+ RSpec::Core::RakeTask.new(:active_record_resque_spec) do |t|
19
+ t.rspec_opts = ['--options', "\"./.rspec\""]
20
+ t.pattern = "spec/activerecord/resque/*_spec.rb"
21
+ end
22
+
23
+ RSpec::Core::RakeTask.new(:active_record_sidekiq_spec) do |t|
24
+ t.rspec_opts = ['--options', "\"./.rspec\""]
25
+ t.pattern = "spec/activerecord/sidekiq/*_spec.rb"
26
+ end
27
+
13
28
  RSpec::Core::RakeTask.new(:data_mapper_spec) do |t|
14
29
  t.rspec_opts = ['--options', "\"./.rspec\""]
15
30
  t.pattern = "spec/datamapper/*_spec.rb"
@@ -17,6 +32,9 @@ end
17
32
 
18
33
  task :spec do |t|
19
34
  Rake::Task["active_record_spec"].invoke rescue (failed = true)
35
+ Rake::Task["active_record_delayed_job_spec"].invoke rescue (failed = true)
36
+ Rake::Task["active_record_resque_spec"].invoke rescue (failed = true)
37
+ Rake::Task["active_record_sidekiq_spec"].invoke rescue (failed = true) unless RUBY_VERSION == "1.8.7"
20
38
  Rake::Task["data_mapper_spec"].invoke rescue (failed = true)
21
39
  raise "failed" if failed
22
40
  end
@@ -25,10 +25,10 @@ Gem::Specification.new do |gem|
25
25
  gem.require_paths = ["lib"]
26
26
 
27
27
  gem.add_development_dependency("rake",["~> 0.9.2.2"])
28
- gem.add_development_dependency("rack-test",["=0.6.2"])
28
+ gem.add_development_dependency("rack-test",["~> 0.6.2"])
29
29
  gem.add_development_dependency("rspec",["~> 2.11.0"])
30
- gem.add_development_dependency("database_cleaner",["=0.8.0"])
31
- gem.add_development_dependency("colorize",["=0.5.8"])
30
+ gem.add_development_dependency("database_cleaner",["~> 0.8.0"])
31
+ gem.add_development_dependency("colorize",["~> 0.5.8"])
32
32
  gem.add_development_dependency("pry")
33
33
  gem.add_development_dependency("pry-doc")
34
34
  gem.add_development_dependency("awesome_print")
@@ -7,6 +7,7 @@ require "association_observers/ruby18" if RUBY_VERSION < "1.9"
7
7
  require "active_support/core_ext/array/extract_options"
8
8
  require "active_support/core_ext/string/inflections"
9
9
 
10
+
10
11
  # Here it is defined the basic behaviour of how observer/observable model associations are set. There are here three
11
12
  # main roles defined: The observer associations, the observable associations, and the notifiers (the real observers).
12
13
  # Observer Associations: those are the associations of an observable which will be "listening/observing" to updates
@@ -24,34 +25,30 @@ require "active_support/core_ext/string/inflections"
24
25
  #
25
26
  # @author Tiago Cardoso
26
27
  module AssociationObservers
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"
28
+ autoload :Queue, "association_observers/queue"
29
+ module Workers
30
+ autoload :ManyDelayedNotification, "association_observers/workers/many_delayed_notification"
32
31
  end
33
32
 
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"
33
+ def self.orm_adapter
34
+ raise "no adapter for your ORM"
38
35
  end
39
36
 
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"
37
+ def self.queue
38
+ @queue ||= Queue.instance
44
39
  end
45
40
 
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"
41
+ @default_options = {
42
+ :batch_size => 50,
43
+ :queue => {
44
+ :engine => nil,
45
+ :name => "observers",
46
+ :priority => nil
47
+ }
48
+ }
49
+
50
+ def self.options
51
+ @options ||= @default_options.dup
55
52
  end
56
53
 
57
54
 
@@ -62,11 +59,24 @@ module AssociationObservers
62
59
 
63
60
  # Methods to be added to observer associations
64
61
  module IsObserverMethods
65
- def self.included(base) ; base.extend(ClassMethods) ; end
62
+ def self.included(base)
63
+ base.extend(ClassMethods)
64
+ AssociationObservers::orm_adapter.class_variable_set(base, :observable_options)
65
+ if RUBY_VERSION < "1.9"
66
+ base.observable_options = AssociationObservers::Backports.hash_select(AssociationObservers::options){|k, v| [:batch_size].include?(k) }
67
+ else
68
+ base.observable_options = AssociationObservers::options.select{|k, v| [:batch_size].include?(k) }
69
+ end
70
+ end
66
71
 
67
72
  module ClassMethods
68
73
  def observer? ; true ; end
69
74
 
75
+ def batch_size=(val)
76
+ raise "AssociationObservers: it must be an integer value" unless val.is_a?(Fixnum)
77
+ self.observable_options[:batch_size] = val
78
+ end
79
+
70
80
  private
71
81
 
72
82
  # @abstract
@@ -139,7 +149,9 @@ module AssociationObservers
139
149
  end
140
150
  end
141
151
 
152
+ # blocks the observable behaviour
142
153
  def unobservable! ; @unobservable = true ; end
154
+ # unblocks the observable behaviour
143
155
  def observable! ; @unobservable = false ; end
144
156
 
145
157
  private
@@ -147,7 +159,7 @@ module AssociationObservers
147
159
  # informs the observers that something happened on this observable, passing all the observers to it
148
160
  # @param [Symbol] callback key of the callback being notified; only the observers for this callback will be run
149
161
  def notify! callback
150
- notify_observers(callback) unless @unobservable
162
+ notify_observers([callback, []]) unless @unobservable
151
163
  end
152
164
  end
153
165
 
@@ -185,9 +197,7 @@ module AssociationObservers
185
197
  observer_callbacks = Array(opts[:on] || [:save, :destroy])
186
198
 
187
199
  # no observer, how are you supposed to observe?
188
- AssociationObservers::validate_parameters(self, args, notifier_classes, observer_callbacks)
189
-
190
-
200
+ AssociationObservers::orm_adapter.validate_parameters(self, args, notifier_classes, observer_callbacks)
191
201
 
192
202
 
193
203
  notifier_classes.map!{|notifier_class|notifier_class.to_s.classify.constantize} << PropagationNotifier
@@ -198,7 +208,7 @@ module AssociationObservers
198
208
  end
199
209
 
200
210
  # 1: for each observed association, define behaviour
201
- get_association_options_pairs(args).each do |klass, options|
211
+ get_association_options_pairs(args).each do |name, klass, options|
202
212
  klass.instance_eval do
203
213
 
204
214
  include IsObservableMethods
@@ -208,7 +218,7 @@ module AssociationObservers
208
218
  attr_reader :unobservable
209
219
 
210
220
  # load observers from this observable association
211
- set_observers(notifier_classes, observer_callbacks, observer_class, (options[:as] || association_name).to_s)
221
+ set_observers(notifier_classes, observer_callbacks, observer_class, (options[:as] || association_name).to_s, name)
212
222
 
213
223
  # sets the callbacks to inform observers
214
224
  set_notification_on_callbacks(observer_callbacks)
@@ -229,6 +239,8 @@ end
229
239
  if defined?(Rails::Railtie) # RAILS
230
240
  require 'association_observers/railtie'
231
241
  else
232
- require 'association_observers/activerecord' if defined?(ActiveRecord)
233
- require 'association_observers/datamapper' if defined?(DataMapper)
242
+ # ORM Adapters
243
+ require 'association_observers/active_record' if defined?(ActiveRecord)
244
+ require 'association_observers/data_mapper' if defined?(DataMapper)
245
+
234
246
  end
@@ -2,20 +2,12 @@
2
2
  if defined?(ActiveRecord)
3
3
 
4
4
  module AssociationObservers
5
- def self.check_new_record_method
6
- :new_record?
5
+ module Orm
6
+ autoload :ActiveRecord, "association_observers/orm/active_record"
7
7
  end
8
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)}
9
+ def self.orm_adapter
10
+ @orm_adapter ||= Orm::ActiveRecord
19
11
  end
20
12
 
21
13
  # translation of AR callbacks to collection callbacks; we want to ignore the update on collections because neither
@@ -36,7 +28,12 @@ if defined?(ActiveRecord)
36
28
 
37
29
  private
38
30
 
39
- def set_observers(notifiers, callbacks, observer_class, association_name)
31
+ # given the fetched information, it initializes the notifiers
32
+ # @param [Array] notifiers notifiers for the current class
33
+ # @param [Array] callbacks valid callbacks for the notifiers
34
+ # @param [Class] observer_class the class of the observer
35
+ # @param [Symbol] association_name the observer identifier on the observable
36
+ def set_observers(notifiers, callbacks, observer_class, association_name, observable_association_name)
40
37
  notifiers.each do |notifier|
41
38
  callbacks.each do |callback|
42
39
  options = {}
@@ -44,6 +41,8 @@ if defined?(ActiveRecord)
44
41
  self.reflect_on_association(association_name.pluralize.to_sym)
45
42
  options[:observer_class] = observer_class.base_class if observer_association.options[:polymorphic]
46
43
 
44
+ options[:observable_association_name] = observable_association_name
45
+
47
46
  self.add_observer notifier.new(callback, observer_association.name, options)
48
47
  include "#{notifier.name}::ObservableMethods".constantize if notifier.constants.map(&:to_sym).include?(:ObservableMethods)
49
48
  end
@@ -74,7 +73,7 @@ if defined?(ActiveRecord)
74
73
  private
75
74
 
76
75
  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] }
76
+ reflect_on_all_associations.select{ |r| association_names.include?(r.name) }.map{|r| [r.name, r.klass, r.options] }
78
77
  end
79
78
 
80
79
  def filter_collection_associations(associations)
@@ -113,8 +112,8 @@ if defined?(ActiveRecord)
113
112
 
114
113
  # bullshit ruby 1.8 can't stringify hashes, arrays, symbols nor strings correctly
115
114
  if RUBY_VERSION < "1.9"
116
- assoc_options = AssociationObservers::extended_to_s(a.options)
117
- callback_options = AssociationObservers::extended_to_s(callbacks)
115
+ assoc_options = AssociationObservers::Backports::extended_to_s(a.options)
116
+ callback_options = AssociationObservers::Backports::extended_to_s(callbacks)
118
117
  else
119
118
  assoc_options = a.options.to_s
120
119
  callback_options = callbacks
@@ -1,24 +1,12 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  if defined?(DataMapper)
3
3
  module AssociationObservers
4
- def self.check_new_record_method
5
- :new?
4
+ module Orm
5
+ autoload :DataMapper, "association_observers/orm/data_mapper"
6
6
  end
7
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
8
+ def self.orm_adapter
9
+ @orm_adapter ||= Orm::DataMapper
22
10
  end
23
11
 
24
12
  module IsObservableMethods
@@ -33,12 +21,15 @@ if defined?(DataMapper)
33
21
  end
34
22
  private
35
23
 
36
- def set_observers(ntfs, callbacks, observer_class, association_name)
24
+ def set_observers(ntfs, callbacks, observer_class, association_name, observable_association_name)
37
25
  ntfs.each do |notifier|
38
26
  callbacks.each do |callback|
39
27
  options = {} # todo: use this for polymorphics
40
28
  observer_association = self.relationships[association_name]||
41
29
  self.relationships[association_name.pluralize]
30
+
31
+ options[:observable_association_name] = observable_association_name
32
+
42
33
  notifiers << notifier.new(callback, observer_association.name, options)
43
34
  include "#{notifier.name}::ObservableMethods".constantize if notifier.constants.map(&:to_sym).include?(:ObservableMethods)
44
35
  end
@@ -61,8 +52,8 @@ if defined?(DataMapper)
61
52
  private
62
53
 
63
54
 
64
- def notify_observers(callback)
65
- self.class.notifiers.each{|notifier| notifier.update(callback, self)}
55
+ def notify_observers(args)
56
+ self.class.notifiers.each{|notifier| notifier.update(args, self)}
66
57
  end
67
58
  end
68
59
  end
@@ -79,7 +70,7 @@ if defined?(DataMapper)
79
70
 
80
71
  def get_association_options_pairs(association_names)
81
72
  # 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] }
73
+ relationships.select{|r|association_names.include?(r.name)}.map{|r| [r.name, (r.is_a?(DataMapper::Associations::ManyToOne::Relationship) ? r.parent_model : r.child_model), r.options] }
83
74
  end
84
75
 
85
76
  def filter_collection_associations(associations)
@@ -0,0 +1,13 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module AssociationObservers
4
+ class Queue
5
+ private
6
+
7
+ # overwriting the enqueue method. Delayed Job enqueues the jobs already itself
8
+ def enqueue(task, *args)
9
+ job = task.new(*args)
10
+ Delayed::Job.enqueue job, :queue => AssociationObservers::options[:queue][:name], :priority => AssociationObservers::options[:queue][:priority] || Delayed::Worker.default_priority
11
+ end
12
+ end
13
+ end