reactor 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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