reactor 0.13.0 → 0.14.0

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +102 -17
  3. data/lib/reactor/controllers/concerns/actions/action_event.rb +11 -7
  4. data/lib/reactor/controllers/concerns/actions/create_event.rb +16 -12
  5. data/lib/reactor/controllers/concerns/actions/destroy_event.rb +8 -4
  6. data/lib/reactor/controllers/concerns/actions/edit_event.rb +8 -4
  7. data/lib/reactor/controllers/concerns/actions/index_event.rb +8 -4
  8. data/lib/reactor/controllers/concerns/actions/new_event.rb +8 -4
  9. data/lib/reactor/controllers/concerns/actions/show_event.rb +7 -3
  10. data/lib/reactor/controllers/concerns/actions/update_event.rb +16 -12
  11. data/lib/reactor/controllers/concerns/resource_actionable.rb +33 -31
  12. data/lib/reactor/controllers.rb +2 -0
  13. data/lib/reactor/errors.rb +7 -0
  14. data/lib/reactor/event.rb +2 -2
  15. data/lib/reactor/models/concerns/subscribable.rb +32 -79
  16. data/lib/reactor/models/subscriber.rb +21 -19
  17. data/lib/reactor/models.rb +5 -0
  18. data/lib/reactor/static_subscribers.rb +7 -0
  19. data/lib/reactor/subscription.rb +100 -0
  20. data/lib/reactor/testing.rb +50 -0
  21. data/lib/reactor/version.rb +1 -1
  22. data/lib/reactor/workers/database_subscriber_worker.rb +22 -0
  23. data/lib/reactor/workers/event_worker.rb +65 -0
  24. data/lib/reactor/workers/mailer_worker.rb +80 -0
  25. data/lib/reactor/workers.rb +8 -0
  26. data/lib/reactor.rb +24 -34
  27. data/reactor.gemspec +5 -1
  28. data/spec/event_spec.rb +10 -2
  29. data/spec/models/concerns/publishable_spec.rb +47 -38
  30. data/spec/models/concerns/subscribable_spec.rb +61 -5
  31. data/spec/models/subscriber_spec.rb +9 -2
  32. data/spec/reactor_spec.rb +2 -0
  33. data/spec/spec_helper.rb +19 -3
  34. data/spec/subscription_spec.rb +55 -0
  35. data/spec/support/active_record.rb +10 -0
  36. data/spec/workers/database_subscriber_worker_spec.rb +67 -0
  37. data/spec/workers/event_worker_spec.rb +126 -0
  38. data/spec/workers/mailer_worker_spec.rb +49 -0
  39. metadata +37 -5
@@ -0,0 +1,100 @@
1
+ module Reactor
2
+ class Subscription
3
+
4
+ attr_reader :source, :event_name, :action, :handler_name, :delay, :async, :worker_class
5
+
6
+ def self.build_handler_name(event_name, handler_name_option = nil)
7
+ if handler_name_option
8
+ handler_name_option.to_s.camelize
9
+ elsif event_name == '*'
10
+ 'WildcardHandler'
11
+ else
12
+ "#{event_name.to_s.camelize}Handler"
13
+ end
14
+ end
15
+
16
+ def initialize(options = {}, &block)
17
+ @source = options[:source]
18
+ @handler_name = self.class.build_handler_name(
19
+ options[:event_name], options[:handler_name]
20
+ )
21
+
22
+ @event_name = options[:event_name]
23
+ @action = options[:action] || block
24
+
25
+ @delay = options[:delay].to_i
26
+ @async = determine_async(options)
27
+ build_worker_class
28
+ end
29
+
30
+ def handler_defined?
31
+ namespace.const_defined?(handler_name) &&
32
+ namespace.const_get(handler_name).ancestors.include?(Reactor.subscriber_namespace)
33
+ end
34
+
35
+ def event_handler_names
36
+ @event_handler_names ||= []
37
+ end
38
+
39
+ def namespace
40
+ return @namespace if @namespace
41
+
42
+ ns = source.name.demodulize
43
+ unless Reactor.subscriber_namespace.const_defined?(ns, false)
44
+ Reactor.subscriber_namespace.const_set(ns, Module.new)
45
+ end
46
+
47
+ @namespace = Reactor.subscriber_namespace.const_get(ns, false)
48
+ end
49
+
50
+ def mailer_subscriber?
51
+ !!(source < ActionMailer::Base)
52
+ end
53
+
54
+ private
55
+
56
+ # options[:in_memory] is a legacy way of setting async to false -
57
+ # see Reactor::Workers::EventWorker#perform_where_needed
58
+ def determine_async(options = {})
59
+ if options[:async].nil?
60
+ if options[:in_memory].nil?
61
+ true
62
+ else
63
+ !options[:in_memory]
64
+ end
65
+ else
66
+ !!options[:async]
67
+ end
68
+ end
69
+
70
+ def build_worker_class
71
+ return @worker_class = namespace.const_get(handler_name) if handler_defined?
72
+
73
+
74
+ worker_class = mailer_subscriber? ? build_mailer_worker : build_event_worker
75
+ namespace.const_set(handler_name, worker_class)
76
+ @worker_class = namespace.const_get(handler_name)
77
+ end
78
+
79
+ def build_event_worker
80
+ subscription = self
81
+ Class.new(Reactor::Workers::EventWorker) do
82
+ self.source = subscription.source
83
+ self.action = subscription.action
84
+ self.async = subscription.async
85
+ self.delay = subscription.delay
86
+ end
87
+ end
88
+
89
+ def build_mailer_worker
90
+ subscription = self
91
+ Class.new(Reactor::Workers::MailerWorker) do
92
+ self.source = subscription.source
93
+ self.action = subscription.action
94
+ self.delay = subscription.delay
95
+ self.async = subscription.async
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,50 @@
1
+ module Reactor
2
+ TEST_MODE_SUBSCRIBERS = Set.new
3
+ @@test_mode = false
4
+
5
+ module_function
6
+
7
+ def test_mode?
8
+ @@test_mode
9
+ end
10
+
11
+ def test_mode!
12
+ @@test_mode = true
13
+ end
14
+
15
+ def disable_test_mode!
16
+ @@test_mode = false
17
+ end
18
+
19
+ def in_test_mode
20
+ test_mode!
21
+ (yield if block_given?).tap { disable_test_mode! }
22
+ end
23
+
24
+ def test_mode_subscribers
25
+ TEST_MODE_SUBSCRIBERS
26
+ end
27
+
28
+ def enable_test_mode_subscriber(klass)
29
+ test_mode_subscribers << klass
30
+ end
31
+
32
+ def disable_test_mode_subscriber(klass)
33
+ test_mode_subscribers.delete klass
34
+ end
35
+
36
+ def with_subscriber_enabled(klass)
37
+ enable_test_mode_subscriber klass
38
+ yield if block_given?
39
+ ensure
40
+ disable_test_mode_subscriber klass
41
+ end
42
+
43
+ def clear_test_subscribers!
44
+ test_mode_subscribers.each {|klass| test_mode_subscribers.delete klass }
45
+ end
46
+
47
+ def test_mode_subscriber_enabled?(subscriber)
48
+ test_mode_subscribers.include?(subscriber)
49
+ end
50
+ end
@@ -1,3 +1,3 @@
1
1
  module Reactor
2
- VERSION = "0.13.0"
2
+ VERSION = "0.14.0"
3
3
  end
@@ -0,0 +1,22 @@
1
+ module Reactor
2
+ module Workers
3
+ class DatabaseSubscriberWorker
4
+
5
+ include Sidekiq::Worker
6
+
7
+ def perform(model_id, data)
8
+ return :__perform_aborted__ unless should_perform?
9
+ Reactor::Subscriber.fire(model_id, data)
10
+ end
11
+
12
+ def should_perform?
13
+ if Reactor.test_mode?
14
+ Reactor.test_mode_subscriber_enabled? Reactor::Subscriber
15
+ else
16
+ true
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,65 @@
1
+ =begin
2
+ EventWorker is an abstract worker for handling events defined by on_event.
3
+ You can create handlers by subclassing and redefining the configuration class
4
+ methods, or by using Reactor::Workers::EventWorker.dup and overriding the
5
+ methods on the new class.
6
+ =end
7
+ module Reactor
8
+ module Workers
9
+ class EventWorker
10
+
11
+ include Sidekiq::Worker
12
+
13
+ CONFIG = [:source, :action, :async, :delay]
14
+
15
+ class_attribute *CONFIG
16
+
17
+ def self.configured?
18
+ CONFIG.all? {|field| !self.send(field).nil? }
19
+ end
20
+
21
+ def self.perform_where_needed(data)
22
+ if delay > 0
23
+ perform_in(delay, data)
24
+ elsif async
25
+ perform_async(data)
26
+ else
27
+ new.perform(data)
28
+ end
29
+ source
30
+ end
31
+
32
+ def configured?
33
+ self.class.configured?
34
+ end
35
+
36
+ def perform(data)
37
+ raise_unconfigured! unless configured?
38
+ return :__perform_aborted__ unless should_perform?
39
+ event = Reactor::Event.new(data)
40
+ if action.is_a?(Symbol)
41
+ source.send(action, event)
42
+ else
43
+ action.call(event)
44
+ end
45
+ end
46
+
47
+ def should_perform?
48
+ if Reactor.test_mode?
49
+ Reactor.test_mode_subscriber_enabled? source
50
+ else
51
+ true
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def raise_unconfigured!
58
+ settings = Hash[CONFIG.map {|s| [s, self.class.send(s)] }]
59
+ raise UnconfiguredWorkerError.new(
60
+ "#{self.class.name} is not properly configured! Here are the settings: #{settings}"
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ =begin
2
+ MailerWorker has a bit more to do than EventWorker. It has to run the event, then if the
3
+ output is a Mail::Message or the like it needs to deliver it like ActionMailer would
4
+ =end
5
+ module Reactor
6
+ module Workers
7
+ class MailerWorker
8
+
9
+ include Sidekiq::Worker
10
+
11
+ CONFIG = [:source, :action, :async, :delay]
12
+
13
+ class_attribute *CONFIG
14
+
15
+ def self.configured?
16
+ CONFIG.all? {|field| field.present? }
17
+ end
18
+
19
+ def self.perform_where_needed(data)
20
+ if delay > 0
21
+ perform_in(delay, data)
22
+ elsif async
23
+ perform_async(data)
24
+ else
25
+ new.perform(data)
26
+ end
27
+ source
28
+ end
29
+
30
+ def configured?
31
+ self.class.configured?
32
+ end
33
+
34
+ def perform(data)
35
+ raise_unconfigured! unless configured?
36
+ return :__perform_aborted__ unless should_perform?
37
+ event = Reactor::Event.new(data)
38
+
39
+ msg = if action.is_a?(Symbol)
40
+ source.send(action, event)
41
+ else
42
+ source.class_exec event, &action
43
+ end
44
+
45
+ deliverable?(msg) ? deliver(msg) : msg
46
+ end
47
+
48
+ def deliver(msg)
49
+ if msg.respond_to?(:deliver_now)
50
+ # Rails 4.2/5.0
51
+ msg.deliver_now
52
+ else
53
+ # Rails 3.2/4.0/4.1 + Generic Mail::Message
54
+ msg.deliver
55
+ end
56
+ end
57
+
58
+ def deliverable?(msg)
59
+ msg.respond_to?(:deliver_now) || msg.respond_to?(:deliver)
60
+ end
61
+
62
+ def should_perform?
63
+ if Reactor.test_mode?
64
+ Reactor.test_mode_subscriber_enabled? source
65
+ else
66
+ true
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def raise_unconfigured!
73
+ settings = Hash[CONFIG.map {|s| [s, self.class.send(s)] }]
74
+ raise UnconfiguredWorkerError.new(
75
+ "#{self.class.name} is not properly configured! Here are the settings: #{settings}"
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,8 @@
1
+ module Reactor
2
+ module Workers
3
+ end
4
+ end
5
+
6
+ require "reactor/workers/event_worker"
7
+ require "reactor/workers/mailer_worker"
8
+ require "reactor/workers/database_subscriber_worker"
data/lib/reactor.rb CHANGED
@@ -1,49 +1,39 @@
1
+ require "active_record"
2
+ require "active_support/hash_with_indifferent_access"
3
+ require "action_mailer"
4
+
1
5
  require "reactor/version"
2
- require "reactor/models/concerns/publishable"
3
- require "reactor/models/concerns/subscribable"
4
- require "reactor/models/concerns/optionally_subclassable"
5
- require "reactor/models/subscriber"
6
- require "reactor/controllers/concerns/resource_actionable"
6
+ require "reactor/errors"
7
+ require "reactor/static_subscribers"
8
+ require "reactor/workers"
9
+ require "reactor/subscription"
10
+ require "reactor/models"
11
+ require "reactor/controllers"
7
12
  require "reactor/event"
8
13
 
9
- module Reactor
10
- SUBSCRIBERS = {}
11
- TEST_MODE_SUBSCRIBERS = Set.new
12
- @@test_mode = false
14
+ # FIXME: should only be included in test environments
15
+ require "reactor/testing"
13
16
 
14
- module StaticSubscribers
15
- end
16
-
17
- def self.test_mode?
18
- @@test_mode
19
- end
20
-
21
- def self.test_mode!
22
- @@test_mode = true
23
- end
17
+ module Reactor
18
+ SUBSCRIBERS = {}.with_indifferent_access
24
19
 
25
- def self.disable_test_mode!
26
- @@test_mode = false
27
- end
20
+ module_function
28
21
 
29
- def self.in_test_mode
30
- test_mode!
31
- (yield if block_given?).tap { disable_test_mode! }
22
+ def subscribers
23
+ SUBSCRIBERS
32
24
  end
33
25
 
34
- def self.enable_test_mode_subscriber(klass)
35
- TEST_MODE_SUBSCRIBERS << klass
26
+ def add_subscriber(event_name, worker_class)
27
+ subscribers[event_name] ||= []
28
+ subscribers[event_name] << worker_class
36
29
  end
37
30
 
38
- def self.disable_test_mode_subscriber(klass)
39
- TEST_MODE_SUBSCRIBERS.delete klass
31
+ def subscribers_for(event_name)
32
+ Array(subscribers[event_name]) + Array(subscribers['*'])
40
33
  end
41
34
 
42
- def self.with_subscriber_enabled(klass)
43
- enable_test_mode_subscriber klass
44
- yield if block_given?
45
- ensure
46
- disable_test_mode_subscriber klass
35
+ def subscriber_namespace
36
+ Reactor::StaticSubscribers
47
37
  end
48
38
  end
49
39
 
data/reactor.gemspec CHANGED
@@ -19,7 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "sidekiq"
22
- spec.add_dependency 'activerecord', '~> 5.0.1'
22
+
23
+ rails_version = '~> 5.0.2'
24
+
25
+ spec.add_dependency 'rails', rails_version
23
26
 
24
27
  spec.add_development_dependency "bundler"
25
28
  spec.add_development_dependency "rake"
@@ -29,4 +32,5 @@ Gem::Specification.new do |spec|
29
32
  spec.add_development_dependency "pry-byebug"
30
33
  spec.add_development_dependency "sqlite3"
31
34
  spec.add_development_dependency "test_after_commit"
35
+ spec.add_development_dependency "simplecov"
32
36
  end
data/spec/event_spec.rb CHANGED
@@ -63,8 +63,16 @@ describe Reactor::Event do
63
63
  end
64
64
 
65
65
  describe 'perform' do
66
- before { Reactor::Subscriber.create(event_name: :user_did_this) }
67
- after { Reactor::Subscriber.destroy_all }
66
+ before do
67
+ Reactor::Subscriber.create(event_name: :user_did_this)
68
+ Reactor.enable_test_mode_subscriber(Reactor::Subscriber)
69
+ end
70
+
71
+ after do
72
+ Reactor::Subscriber.destroy_all
73
+ Reactor.enable_test_mode_subscriber(Reactor::Subscriber)
74
+ end
75
+
68
76
  it 'fires all subscribers' do
69
77
  expect_any_instance_of(Reactor::Subscriber).to receive(:fire).with(hash_including(actor_id: model.id.to_s))
70
78
  Reactor::Event.perform(event_name, actor_id: model.id.to_s, actor_type: model.class.to_s)
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
  require 'sidekiq/testing'
3
3
 
4
- class Auction < ActiveRecord::Base
4
+ class Publisher < ActiveRecord::Base
5
5
  belongs_to :pet
6
6
 
7
7
  def ring_timeout
@@ -35,33 +35,33 @@ describe Reactor::Publishable do
35
35
 
36
36
  describe 'publish' do
37
37
  let(:pet) { Pet.create! }
38
- let(:auction) { Auction.create!(pet: pet, start_at: Time.current + 1.day, we_want_it: false) }
38
+ let(:publisher) { Publisher.create!(pet: pet, start_at: Time.current + 1.day, we_want_it: false) }
39
39
 
40
40
  it 'publishes an event with actor_id and actor_type set as self' do
41
- auction
42
- expect(Reactor::Event).to receive(:publish).with(:an_event, what: 'the', actor: auction)
43
- auction.publish(:an_event, {what: 'the'})
41
+ publisher
42
+ expect(Reactor::Event).to receive(:publish).with(:an_event, what: 'the', actor: publisher)
43
+ publisher.publish(:an_event, {what: 'the'})
44
44
  end
45
45
 
46
46
  it 'publishes an event with provided actor and target methods' do
47
47
  allow(Reactor::Event).to receive(:publish).exactly(5).times
48
- auction
49
- expect(Reactor::Event).to have_received(:publish).with(:woof, a_hash_including(actor: pet, target: auction))
48
+ publisher
49
+ expect(Reactor::Event).to have_received(:publish).with(:woof, a_hash_including(actor: pet, target: publisher))
50
50
  end
51
51
 
52
52
  it 'reschedules an event when the :at time changes' do
53
- start_at = auction.start_at
53
+ start_at = publisher.start_at
54
54
  new_start_at = start_at + 1.week
55
55
 
56
56
  allow(Reactor::Event).to receive(:reschedule)
57
57
 
58
- auction.start_at = new_start_at
59
- auction.save!
58
+ publisher.start_at = new_start_at
59
+ publisher.save!
60
60
 
61
61
  expect(Reactor::Event).to have_received(:reschedule).with(:begin,
62
62
  a_hash_including(
63
63
  at: new_start_at,
64
- actor: auction,
64
+ actor: publisher,
65
65
  was: start_at,
66
66
  additional_info: 'curtis was here'
67
67
  )
@@ -69,19 +69,19 @@ describe Reactor::Publishable do
69
69
  end
70
70
 
71
71
  it 'reschedules an event when the :watch field changes' do
72
- ring_time = auction.ring_timeout
73
- new_start_at = auction.start_at + 1.week
72
+ ring_time = publisher.ring_timeout
73
+ new_start_at = publisher.start_at + 1.week
74
74
  new_ring_time = new_start_at + 30.seconds
75
75
 
76
76
  allow(Reactor::Event).to receive(:reschedule)
77
77
 
78
- auction.start_at = new_start_at
79
- auction.save!
78
+ publisher.start_at = new_start_at
79
+ publisher.save!
80
80
 
81
81
  expect(Reactor::Event).to have_received(:reschedule).with(:ring,
82
82
  a_hash_including(
83
83
  at: new_ring_time,
84
- actor: auction,
84
+ actor: publisher,
85
85
  was: ring_time
86
86
  )
87
87
  )
@@ -92,7 +92,7 @@ describe Reactor::Publishable do
92
92
  Sidekiq::Testing.fake!
93
93
  Sidekiq::Worker.clear_all
94
94
  TestSubscriber.create! event_name: :conditional_event_on_save
95
- auction
95
+ publisher
96
96
  job = Reactor::Event.jobs.detect do |job|
97
97
  job['class'] == 'Reactor::Event' && job['args'].first == 'conditional_event_on_save'
98
98
  end
@@ -104,33 +104,35 @@ describe Reactor::Publishable do
104
104
  end
105
105
 
106
106
  it 'calls the subscriber when if is set to true' do
107
- auction.we_want_it = true
108
- auction.start_at = 3.day.from_now
109
- auction.save!
107
+ publisher.we_want_it = true
108
+ publisher.start_at = 3.day.from_now
109
+ allow(Reactor::Event).to receive(:perform_at)
110
+ publisher.save!
111
+ expect(Reactor::Event).to have_received(:perform_at).with(publisher.start_at, :conditional_event_on_save, anything())
110
112
 
111
- expect{ Reactor::Event.perform(@job_args[0], @job_args[1]) }.to change{ Sidekiq::Extensions::DelayedClass.jobs.size }
113
+ Reactor::Event.perform(@job_args[0], @job_args[1])
112
114
  end
113
115
 
114
116
  it 'does not call the subscriber when if is set to false' do
115
- auction.we_want_it = false
116
- auction.start_at = 3.days.from_now
117
- auction.save!
117
+ publisher.we_want_it = false
118
+ publisher.start_at = 3.days.from_now
119
+ publisher.save!
118
120
 
119
121
  expect{ Reactor::Event.perform(@job_args[0], @job_args[1]) }.to_not change{ Sidekiq::Extensions::DelayedClass.jobs.size }
120
122
  end
121
123
 
122
124
  it 'keeps the if intact when rescheduling' do
123
- old_start_at = auction.start_at
124
- auction.start_at = 3.day.from_now
125
+ old_start_at = publisher.start_at
126
+ publisher.start_at = 3.day.from_now
125
127
  allow(Reactor::Event).to receive(:publish)
126
128
  expect(Reactor::Event).to receive(:publish).with(:conditional_event_on_save, {
127
- at: auction.start_at,
128
- actor: auction,
129
+ at: publisher.start_at,
130
+ actor: publisher,
129
131
  target: nil,
130
132
  was: old_start_at,
131
133
  if: anything
132
134
  })
133
- auction.save!
135
+ publisher.save!
134
136
  end
135
137
 
136
138
  it 'keeps the if intact when scheduling' do
@@ -142,25 +144,32 @@ describe Reactor::Publishable do
142
144
  target: nil,
143
145
  if: anything
144
146
  })
145
- Auction.create!(start_at: start_at)
147
+ Publisher.create!(start_at: start_at)
146
148
  end
147
149
  end
148
150
 
149
151
  it 'supports immediate events (on create) that get fired once' do
150
- TestSubscriber.create! event_name: :bell
151
- auction
152
- expect(TestSubscriber.class_variable_get(:@@called)).to be_truthy
153
- TestSubscriber.class_variable_set(:@@called, false)
154
- auction.start_at = 1.day.from_now
155
- auction.save
156
- expect(TestSubscriber.class_variable_get(:@@called)).to be_falsey
152
+ Reactor.with_subscriber_enabled(Reactor::Subscriber) do
153
+ TestSubscriber.create! event_name: :bell
154
+ publisher
155
+ expect(TestSubscriber.class_variable_get(:@@called)).to be_truthy
156
+ TestSubscriber.class_variable_set(:@@called, false)
157
+ publisher.start_at = 1.day.from_now
158
+ publisher.save
159
+ expect(TestSubscriber.class_variable_get(:@@called)).to be_falsey
160
+ end
157
161
  end
158
162
 
159
163
  it 'does publish an event scheduled for the future' do
164
+ Reactor.enable_test_mode_subscriber Reactor::Subscriber
165
+ Reactor.enable_test_mode_subscriber Publisher
160
166
  TestSubscriber.create! event_name: :begin
161
- Auction.create!(pet: pet, start_at: Time.current + 1.week)
167
+ Publisher.create!(pet: pet, start_at: Time.current + 1.week)
162
168
 
163
169
  expect(TestSubscriber.class_variable_get(:@@called)).to be_truthy
170
+
171
+ Reactor.disable_test_mode_subscriber Reactor::Subscriber
172
+ Reactor.disable_test_mode_subscriber Publisher
164
173
  end
165
174
  end
166
175
  end