reactor 0.19.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5fb837714e773b0e547099b05ed322b059310f23
4
- data.tar.gz: 928b14c3d60d9cae2a318d2fb36fa84e2e6384da
3
+ metadata.gz: 8bbbeb29447a373e5c2d652170a9d9f57d2251b7
4
+ data.tar.gz: 8034ad54e39372f66f0f13255265e2ac77ccf954
5
5
  SHA512:
6
- metadata.gz: 536da1fe846ac55173a110ffd7e25e1bb362e0ca765ab40dac2730f379521b6b62da64b77e8f1768733fa4ca275c872b7da9df2020389bbe729b02685fc7e52a
7
- data.tar.gz: faf39d4f13d4a2d173284b0298947b92eb7959fc63d800a397df6db2963c22a4a421bf7c796880d22362f74db3175982412d82e08f8ccd5355873b04230cc266
6
+ metadata.gz: 2c1f561926d09bd84826533eab277b0cfb69bc2efe5c20a54b3f2e3ecfe217aeb52cff776d36e4c8174a5348d51e293323d2968f08cf23c7221cdfb57e5cb68e
7
+ data.tar.gz: 5d089b23eefc52622a1fc29db4dbf2c5f17392c46373836d7411517cadf39b15d8b17cc24dc3e5a5ec92e0508b998775f8a5dcaf667db85d8835e1119d4267b2
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reactor (0.19.0)
4
+ reactor (1.0.0)
5
5
  rails
6
6
  sidekiq (> 4.0)
7
7
 
data/README.md CHANGED
@@ -87,15 +87,7 @@ on_event :any_event do |event|
87
87
  end
88
88
  ```
89
89
 
90
- Static subscribers like these are automatically placed into Sidekiq and executed in the background
91
-
92
- It's also possible to run a subscriber block in memory like so
93
-
94
- ```ruby
95
- on_event :any_event, in_memory: true do |event|
96
- event.target.do_something_about_it_and_make_the_user_wait!
97
- end
98
- ```
90
+ Static subscribers like these are automatically placed into Sidekiq and executed in the background.
99
91
 
100
92
  You may also have Sidekiq process a subscriber block on a specific queue or supply any other Sidekiq::Worker options accordingly.
101
93
 
@@ -104,132 +96,13 @@ on_event :event_with_ui_bound, sidekiq_options: { queue: 'highest_priority' } do
104
96
  speedily_execute!
105
97
  end
106
98
  ```
99
+ ### Automatic Events
107
100
 
108
- #### ResourceActionable
101
+ If you'd like to have events automatically fired for you around standard rails resource controller actions,
102
+ you may want to write your own downstream abstraction for it. We attempted this once and it seems unwise to presume your application would want the same thing.
109
103
 
110
- Enforce a strict 1:1 match between your event model and database model with this controller mixin.
111
-
112
-
113
- ```ruby
114
- class PetsController < ApplicationController
115
- include Reactor::ResourceActionable
116
- actionable_resource :@pet
117
-
118
- # GET /pets
119
- # GET /pets.json
120
- def index
121
- @pets = current_user.pets
122
-
123
- respond_to do |format|
124
- format.html # index.html.erb
125
- format.json { render json: @pets }
126
- end
127
- end
128
-
129
- def show
130
- @pet = current_user.pets.find(params[:id])
131
- respond_to do |format|
132
- format.html # index.html.erb
133
- format.json { render json: @pet }
134
- end
135
- end
136
- end
137
-
138
- ```
139
-
140
- Now your index action (and any of the other RESTful actions in that controller) will fire a useful event for you to bind to and log.
141
-
142
- *Important* Reactor::ResourceActionable has one major usage constraints:
143
-
144
- Your controller *must* have a method called "action_event" with this signature.
145
- ```ruby
146
- def action_event(name, options = {})
147
- # Here's what ours looks like, but yours may look different.
148
- actor = options[:actor] || current_user
149
- actor.publish(name, options.merge(default_action_parameters))
150
- #where default_action_parameters includes things like ip_address, referrer, user_agent
151
- end
152
- ```
153
-
154
- Once you write your own action_event to describe your event data model's base attributes, your ResourceActionable endpoints will now fire events that map like so (for the example above):
155
-
156
- <dl>
157
- <dt>index =></dt>
158
- <dd>"pets_indexed"</dd>
159
- </dl>
160
-
161
- <dl>
162
- <dt>show =></dt>
163
- <dd>"pet_viewed", target: @pet</dd>
164
- </dl>
165
-
166
- <dl>
167
- <dt>new =></dt>
168
- <dd>"new_pet_form_viewed"</dd>
169
- </dl>
170
-
171
- <dl>
172
- <dt>edit =></dt>
173
- <dd> "edit_pet_form_viewed", target: @pet</dd>
174
- </dl>
175
-
176
- <dl>
177
- <dt>create =></dt>
178
- <dd> when valid => "pet_created", target: @pet, attributes: params[:pet]
179
- <br />
180
- when invalid => "pet_create_failed", errors: @pet.errors, attributes: params[:pet]</dd>
181
- </dl>
182
-
183
- <dl>
184
- <dt>update =></dt>
185
- <dd>
186
- when valid => "pet_updated", target: @pet, changes: @pet.previous_changes.as_json
187
- <br />
188
- when invalid => "pet_update_failed", target: @pet,
189
- errors: @pet.errors.as_json, attributes: params[:pet]
190
- </dd>
191
- </dl>
192
-
193
- <dl>
194
- <dt>destroy =></dt>
195
- <dd>"pet_destroyed", last_snapshot: @pet.as_jsont</dd>
196
- </dl>
197
-
198
-
199
- ##### What for?
200
-
201
- If you're obsessive about data like us, you'll have written a '*' subscriber that logs every event fired in the system. With information-dense resource information logged for each action a user performs, it will be trivial for a data analyst to determine patterns in user activity. For example, with the above data being logged for the pet resource, we can easily
202
- * determine which form field validations are constantly being hit by users
203
- * see if there are any fields that are consistently ignored on that form until later
204
- * recover data from the last_snapshot of a destroyed record
205
- * write a small conversion funnel analysis to see who never makes it back to a record to update it
206
- * bind arbitrary logic anywhere in the codebase (see next example) to that specific request without worrying about the logic being run during the request (all listeners are run in the background by Sidekiq)
207
-
208
- For example, in an action mailer.
209
-
210
- ```ruby
211
- class MyMailer < ActionMailer::Base
212
- include Reactor::EventMailer
213
-
214
- on_event :pet_created do |event|
215
- @user = event.actor
216
- @pet = event.target
217
- mail to: @user.email, subject: "Your pet is already hungry!", body: "feed it."
218
- end
219
- end
220
- ```
221
-
222
- Or in a model, concern, or other business logic file.
223
-
224
- ```ruby
225
- class MyClass
226
- include Reactor::Subscribable
227
-
228
- on_event :pet_updated do |event|
229
- event.actor.recalculate_expensive_something_for(event.target)
230
- end
231
- end
232
- ```
104
+ It probably only makes sense if you have a real Magestic Monolith and an intentionally small team because coupling resource names to event streams makes refactoring harder.
105
+ (Though it may still be worth it for you, depending on your needs!)
233
106
 
234
107
  ### Testing
235
108
 
@@ -266,12 +139,30 @@ expect { some_thing }.to publish_events(:some_event, :another_event)
266
139
 
267
140
  ### Production Deployments
268
141
 
269
- TLDR; Everything is a Sidekiq::Job, so all the same gotchas apply with regard to removing & renaming jobs that may have a live reference sitting in the queue. (AKA, you'll start seeing 'const undefined' exceptions when the job gets picked up if you've already deleted/renamed the job code.)
142
+ TLDR; Everything is a Sidekiq::Worker, so all the same gotchas apply with regard to removing & renaming jobs that may have a live reference sitting in the queue. (AKA, you'll start seeing 'const undefined' exceptions when the job gets picked up if you've already deleted/renamed the job code.)
270
143
 
271
144
  #### Adding Events and Subscribers
272
145
 
273
146
  This is as easy as write + deploy. Of course your events getting fired won't have a subscriber pick them up until the new subscriber code is deployed in your sidekiq instances, but that's not too surprising.
274
147
 
148
+ #### Validating Events On Publish
149
+
150
+ As of 1.0 you may inject your own validator lambda to handle the logic and flow-control of valid/invalid events.
151
+
152
+ This is entirely optional and the default behavior is to do nothing, to not validate any data being provided.
153
+
154
+ ```ruby
155
+ # in config/initializers/reactor.rb
156
+ Reactor.validator -> do |event|
157
+ Activity.build_from_event(event).validate! # you own the performance implications here
158
+ end
159
+ ```
160
+
161
+ We at Hired use this to validate the event's schema as we found having stricter schema definitions
162
+ gave us more leverage as our team grew.
163
+
164
+ By injecting your own logic, you can be as permissive or strict as you want. (Throw exceptions if you want, even.)
165
+
275
166
  #### Removing Events and Subscribers
276
167
 
277
168
  Removing an event is as simple as deleting the line of code that `publish`es it.
data/lib/reactor.rb CHANGED
@@ -9,14 +9,11 @@ require "reactor/workers/concerns/configuration"
9
9
  require "reactor/workers"
10
10
  require "reactor/subscription"
11
11
  require "reactor/models"
12
- require "reactor/controllers"
13
12
  require "reactor/event"
14
13
 
15
- # FIXME: should only be included in test environments
16
- require "reactor/testing"
17
-
18
14
  module Reactor
19
15
  SUBSCRIBERS = {}.with_indifferent_access
16
+ BASE_VALIDATOR = -> (_event) { } # default behavior is to not actually validate anything
20
17
 
21
18
  module_function
22
19
 
@@ -36,4 +33,17 @@ module Reactor
36
33
  def subscriber_namespace
37
34
  Reactor::StaticSubscribers
38
35
  end
36
+
37
+ #
38
+ # If you want, you can inject your own validator block in `config/initializers/reactor.rb`
39
+ #
40
+ # Reactor.validator -> (event) { Validator.new(event).validate! }
41
+ #
42
+ # If not, the default behavior is to do nothing. (see Reactor::BASE_VALIDATOR)
43
+ #
44
+ def validator(block = nil)
45
+ @validator = block if block.present?
46
+
47
+ @validator || BASE_VALIDATOR
48
+ end
39
49
  end
data/lib/reactor/event.rb CHANGED
@@ -63,8 +63,11 @@ class Reactor::Event
63
63
  if defined?(Rails::Console) && ENV['RACK_ENV'] == 'production' && data[:srsly].blank?
64
64
  raise ArgumentError.new(CONSOLE_CONFIRMATION_MESSAGE)
65
65
  end
66
+
66
67
  message = new(data.merge(event: name, uuid: SecureRandom.uuid))
67
68
 
69
+ Reactor.validator.call(message)
70
+
68
71
  if message.at
69
72
  perform_at message.at, name, message.__data__
70
73
  else
@@ -124,6 +127,8 @@ class Reactor::Event
124
127
  end
125
128
 
126
129
  def fire_block_subscribers(data, name)
127
- ((Reactor::SUBSCRIBERS[name.to_s] || []) | (Reactor::SUBSCRIBERS['*'] || [])).each { |s| s.perform_where_needed(data) }
130
+ ((Reactor::SUBSCRIBERS[name.to_s] || []) | (Reactor::SUBSCRIBERS['*'] || [])).each do |s|
131
+ s.perform_where_needed(data)
132
+ end
128
133
  end
129
134
  end
@@ -1,7 +1,7 @@
1
1
  module Reactor
2
2
  class Subscription
3
3
 
4
- attr_reader :source, :event_name, :action, :handler_name, :delay, :async, :worker_class,
4
+ attr_reader :source, :event_name, :action, :handler_name, :delay, :worker_class,
5
5
  :deprecated, :sidekiq_options
6
6
 
7
7
  def self.build_handler_name(event_name, handler_name_option = nil)
@@ -24,7 +24,6 @@ module Reactor
24
24
  @action = options[:action] || block
25
25
 
26
26
  @delay = options[:delay].to_i
27
- @async = determine_async(options)
28
27
  @deprecated = !!options[:deprecated]
29
28
  @sidekiq_options = options[:sidekiq_options] || {}
30
29
  build_worker_class
@@ -41,13 +40,16 @@ module Reactor
41
40
 
42
41
  def namespace
43
42
  return @namespace if @namespace
43
+ @namespace = generate_namespace
44
+ end
44
45
 
45
- ns = source.name.demodulize
46
- unless Reactor.subscriber_namespace.const_defined?(ns, false)
47
- Reactor.subscriber_namespace.const_set(ns, Module.new)
46
+ def generate_namespace
47
+ module_chain.reduce(Reactor.subscriber_namespace) do |mod, name|
48
+ unless mod.const_defined?(name, false)
49
+ mod.const_set(name, Module.new)
50
+ end
51
+ mod.const_get(name)
48
52
  end
49
-
50
- @namespace = Reactor.subscriber_namespace.const_get(ns, false)
51
53
  end
52
54
 
53
55
  def mailer_subscriber?
@@ -56,24 +58,14 @@ module Reactor
56
58
 
57
59
  private
58
60
 
59
- # options[:in_memory] is a legacy way of setting async to false -
60
- # see Reactor::Workers::EventWorker#perform_where_needed
61
- def determine_async(options = {})
62
- if options[:async].nil?
63
- if options[:in_memory].nil?
64
- true
65
- else
66
- !options[:in_memory]
67
- end
68
- else
69
- !!options[:async]
70
- end
61
+ def module_chain
62
+ source.name.split('::')
71
63
  end
72
64
 
73
65
  def build_worker_class
74
- namespace.send(:remove_const, handler_name) if handler_defined?
75
66
 
76
67
  worker_class = mailer_subscriber? ? build_mailer_worker : build_event_worker
68
+ namespace.send(:remove_const, handler_name) if handler_defined?
77
69
  namespace.const_set(handler_name, worker_class)
78
70
  @worker_class = namespace.const_get(handler_name)
79
71
  end
@@ -83,7 +75,6 @@ module Reactor
83
75
  Class.new(Reactor::Workers::EventWorker) do
84
76
  self.source = subscription.source
85
77
  self.action = subscription.action
86
- self.async = subscription.async
87
78
  self.delay = subscription.delay
88
79
  self.deprecated = subscription.deprecated
89
80
  self.sidekiq_options subscription.sidekiq_options
@@ -96,7 +87,6 @@ module Reactor
96
87
  self.source = subscription.source
97
88
  self.action = subscription.action
98
89
  self.delay = subscription.delay
99
- self.async = subscription.async
100
90
  self.deprecated = subscription.deprecated
101
91
  self.sidekiq_options subscription.sidekiq_options
102
92
  end
@@ -0,0 +1,46 @@
1
+ #
2
+ # Run this before specs if you want to speed up tests by trading out code coverage into subscribers
3
+ #
4
+ def stub_reactor_subscribers
5
+ Reactor::SUBSCRIBERS.each do |_event, subscribers|
6
+ subscribers.each do |subscriber|
7
+ allow(subscriber).to receive(:perform_where_needed)
8
+ end
9
+ end
10
+ end
11
+
12
+ #
13
+ # If stubbing out reactor in test, use this method to re-enable a specific subscriber
14
+ # to test its logic.
15
+ #
16
+ def allow_reactor_subscriber(subscribable_class)
17
+ worker_module_name = "Reactor::StaticSubscribers::#{subscribable_class}"
18
+ worker_module_name.safe_constantize.constants.each do |worker_class_name|
19
+ worker_class = "#{worker_module_name}::#{worker_class_name}".safe_constantize
20
+ allow(worker_class).to receive(:perform_where_needed).and_call_original
21
+ end
22
+
23
+ yield if block_given? # yes you can use block syntax if you want
24
+ end
25
+
26
+ #
27
+ # If you publish events in ActiveRecord lifecycle hooks, you're gonna have a bad time.
28
+ #
29
+ # But inevitably it may make sense for you (yay software), in which case you may want to
30
+ # disable a subscriber if you're testing logic around it.
31
+ #
32
+ def disable_reactor_subscriber(subscribable_class)
33
+ worker_module_name = "Reactor::StaticSubscribers::#{subscribable_class}"
34
+ worker_module_name.safe_constantize.constants.each do |worker_class_name|
35
+ worker_class = "#{worker_module_name}::#{worker_class_name}".safe_constantize
36
+ allow(worker_class).to receive(:perform_where_needed).and_return(nil)
37
+ end
38
+
39
+ if block_given? # yes you can use block syntax if you want
40
+ begin
41
+ yield
42
+ ensure
43
+ allow_reactor_subscriber(subscribable_class) # and if you do, expect it to be re-enabled after
44
+ end
45
+ end
46
+ end
@@ -1,3 +1,3 @@
1
1
  module Reactor
2
- VERSION = "0.19.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -3,7 +3,7 @@ module Reactor
3
3
  module Configuration
4
4
  extend ActiveSupport::Concern
5
5
 
6
- CONFIG = [:source, :action, :async, :delay, :deprecated]
6
+ CONFIG = [:source, :action, :delay, :deprecated]
7
7
 
8
8
  included do
9
9
  include Sidekiq::Worker
@@ -21,10 +21,8 @@ module Reactor
21
21
  return
22
22
  elsif delay > 0
23
23
  event_queue.perform_in(delay, data)
24
- elsif async
25
- event_queue.perform_async(data)
26
24
  else
27
- new.perform(data)
25
+ event_queue.perform_async(data)
28
26
  end
29
27
  source
30
28
  end
@@ -51,11 +49,7 @@ module Reactor
51
49
  end
52
50
 
53
51
  def should_perform?
54
- if Reactor.test_mode?
55
- Reactor.test_mode_subscriber_enabled? source
56
- else
57
- true
58
- end
52
+ true
59
53
  end
60
54
 
61
55
  private
data/spec/event_spec.rb CHANGED
@@ -25,6 +25,7 @@ class OtherWorker
25
25
  end
26
26
 
27
27
  describe Reactor::Event do
28
+ before { stub_reactor_subscribers }
28
29
 
29
30
  let(:model) { ArbitraryModel.create! }
30
31
  let(:event_name) { :user_did_this }
@@ -77,24 +78,32 @@ describe Reactor::Event do
77
78
  ENV['RACK_ENV'] = 'development'
78
79
  end
79
80
  end
81
+
82
+ describe 'using injected validator block' do
83
+ before { Reactor.validator -> (_event) {raise 'InvalidEvent'} }
84
+ after { Reactor.validator Reactor::BASE_VALIDATOR }
85
+
86
+ it 'gets called and yields flow control' do
87
+ expect {
88
+ Reactor::Event.publish(:something)
89
+ }.to raise_error(RuntimeError, 'InvalidEvent')
90
+ end
91
+ end
80
92
  end
81
93
 
82
94
  describe 'perform' do
83
- # before do
84
- # allow(Reactor::Event).
85
- # end
86
95
  let(:event_name) { :barfed }
87
96
 
88
97
  it 'fires all subscribers' do
89
98
  expect(Reactor::StaticSubscribers::ArbitraryModel::BarfedHandler).
90
- to receive(:perform_async).with(hash_including(actor_id: model.id.to_s))
99
+ to receive(:perform_where_needed).with(hash_including(actor_id: model.id.to_s))
91
100
 
92
101
  Reactor::Event.perform(event_name, actor_id: model.id.to_s, actor_type: model.class.to_s)
93
102
  end
94
103
 
95
104
  it 'sets a fired_at key in event data' do
96
105
  expect(Reactor::StaticSubscribers::ArbitraryModel::BarfedHandler).
97
- to receive(:perform_async).with(hash_including(fired_at: anything))
106
+ to receive(:perform_where_needed).with(hash_including(fired_at: a_kind_of(Time)))
98
107
 
99
108
  Reactor::Event.perform(event_name, actor_id: model.id.to_s, actor_type: model.class.to_s)
100
109
  end
@@ -103,7 +112,7 @@ describe Reactor::Event do
103
112
  it 'doesnt matter because it runs in a separate worker process' do
104
113
  expect {
105
114
  Reactor::Event.perform(
106
- 'barfed',
115
+ event_name,
107
116
  somethin: 'up',
108
117
  actor_id: model.id.to_s,
109
118
  actor_type: model.class.to_s
@@ -81,7 +81,6 @@ describe Reactor::Publishable do
81
81
  before do
82
82
  Sidekiq::Testing.fake!
83
83
  Sidekiq::Worker.clear_all
84
- # TestSubscriber.create! event_name: :conditional_event_on_publish
85
84
  publisher
86
85
  job = Reactor::Event.jobs.detect do |job|
87
86
  job['class'] == 'Reactor::Event' && job['args'].first == 'conditional_event_on_publish'
@@ -15,10 +15,6 @@ class Auction < ApplicationRecord
15
15
  event.actor.more_puppies! if event.name == 'another_event'
16
16
  end
17
17
 
18
- on_event :cat_delivered, async: false do |event|
19
- puppies!
20
- end
21
-
22
18
  on_event :a_high_frequency_event, deprecated: true do |event|
23
19
  raise 'hell'
24
20
  end
@@ -49,7 +45,7 @@ class KittenMailer < ActionMailer::Base
49
45
  include Reactor::Subscribable
50
46
 
51
47
  on_event :auction, handler_name: 'auction' do |event|
52
- raise "Event auction"
48
+ @@fired = true
53
49
  end
54
50
 
55
51
  on_event :kitten_streaming do |event|
@@ -67,20 +63,14 @@ class KittenMailer < ActionMailer::Base
67
63
  end
68
64
  end
69
65
 
70
- Reactor.in_test_mode do
71
- class TestModeAuction < ApplicationRecord
72
- on_event :test_puppy_delivered, -> (event) { "success" }
73
- end
66
+ class TestModeAuction < ApplicationRecord
67
+ on_event :test_puppy_delivered, -> (event) { "success" }
74
68
  end
75
69
 
76
70
  describe Reactor::Subscribable do
77
71
  let(:scheduled) { Sidekiq::ScheduledSet.new }
78
72
 
79
73
  describe 'on_event' do
80
- before do
81
- Reactor.enable_test_mode_subscriber(Auction)
82
- end
83
-
84
74
  it 'binds block of code statically to event being fired' do
85
75
  expect_any_instance_of(Auction).to receive(:update_column).with(:status, 'first_bid_made')
86
76
  Reactor::Event.publish(:bid_made, target: Auction.create!(start_at: 10.minutes.from_now))
@@ -92,8 +82,8 @@ describe Reactor::Subscribable do
92
82
  expect(Reactor::SUBSCRIBERS['puppy_delivered'][1]).to eq(Reactor::StaticSubscribers::Auction::DoNothingHandler)
93
83
  end
94
84
 
95
- it 'adds a static subscriber for namespaced classes' do
96
- expect(Reactor::SUBSCRIBERS['rain'][0]).to eq(Reactor::StaticSubscribers::MyClass::RainHandler)
85
+ it 'adds a static subscriber for namespaced classes at the same module depth' do
86
+ expect(Reactor::SUBSCRIBERS['rain'][0]).to eq(Reactor::StaticSubscribers::MyNamespace::MyClass::RainHandler)
97
87
  end
98
88
  end
99
89
 
@@ -101,10 +91,16 @@ describe Reactor::Subscribable do
101
91
  let(:pooped_handler) { Reactor::StaticSubscribers::Auction::PoopedHandler }
102
92
 
103
93
  it 'fires on event' do
94
+ expect(Reactor::StaticSubscribers::Auction::WildcardHandler).
95
+ to receive(:perform_async).and_call_original
104
96
  expect(Auction).to receive(:ring_bell)
105
97
  Reactor::Event.publish(:puppy_delivered)
106
98
  end
107
99
 
100
+ it 'fires perform_async when true / default' do
101
+ Reactor::Event.publish(:puppy_delivered)
102
+ end
103
+
108
104
  it 'can be delayed' do
109
105
  expect(Auction).to receive(:pick_up_poop)
110
106
  expect(pooped_handler).to receive(:perform_in).with(5.minutes, anything).and_call_original
@@ -132,19 +128,6 @@ describe Reactor::Subscribable do
132
128
  expect { Reactor::Event.publish :auction }.not_to raise_error
133
129
  end
134
130
 
135
- describe 'async flag' do
136
- it 'doesnt fire perform_async when false' do
137
- expect(Auction).to receive(:puppies!)
138
- expect(Reactor::StaticSubscribers::Auction::CatDeliveredHandler).not_to receive(:perform_async)
139
- Reactor::Event.publish(:cat_delivered)
140
- end
141
-
142
- it 'fires perform_async when true / default' do
143
- expect(Reactor::StaticSubscribers::Auction::WildcardHandler).to receive(:perform_async)
144
- Reactor::Event.publish(:puppy_delivered)
145
- end
146
- end
147
-
148
131
  describe 'deprecate flag for high-frequency events in production deployments' do
149
132
  it 'doesnt enqueue subscriber worker when true' do
150
133
  # so subscriber can be safely deleted in next deploy
@@ -182,17 +165,8 @@ describe Reactor::Subscribable do
182
165
  end
183
166
 
184
167
  describe '#perform' do
185
- around(:each) do |example|
186
- Reactor.in_test_mode { example.run }
187
- end
188
-
189
- it 'returns :__perform_aborted__ when Reactor is in test mode' do
190
- expect(Reactor::StaticSubscribers::TestModeAuction::TestPuppyDeliveredHandler.new.perform({})).to eq(:__perform_aborted__)
191
- Reactor::Event.publish(:test_puppy_delivered)
192
- end
193
-
194
168
  it 'performs normally when specifically enabled' do
195
- Reactor.with_subscriber_enabled(TestModeAuction) do
169
+ allow_reactor_subscriber(TestModeAuction) do
196
170
  expect(Reactor::StaticSubscribers::TestModeAuction::TestPuppyDeliveredHandler.new.perform({})).not_to eq(:__perform_aborted__)
197
171
  Reactor::Event.publish(:test_puppy_delivered)
198
172
  end
@@ -201,8 +175,6 @@ describe Reactor::Subscribable do
201
175
  end
202
176
 
203
177
  describe 'mailers', type: :mailer do
204
- before { Reactor.enable_test_mode_subscriber KittenMailer }
205
- after { Reactor.disable_test_mode_subscriber KittenMailer }
206
178
 
207
179
  def deliveries
208
180
  ActionMailer::Base.deliveries
data/spec/spec_helper.rb CHANGED
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  require 'reactor'
17
17
  require 'reactor/testing/matchers'
18
+ require 'reactor/testing/stubs'
18
19
 
19
20
  require 'rspec/its'
20
21
 
@@ -53,8 +54,6 @@ RSpec.configure do |config|
53
54
  # some (optional) config here
54
55
 
55
56
  config.before(:each) do
56
- Reactor.test_mode!
57
- Reactor.clear_test_subscribers!
58
57
  ActionMailer::Base.deliveries.clear
59
58
  end
60
59
 
@@ -2,6 +2,31 @@ require 'spec_helper'
2
2
 
3
3
  describe Reactor::Subscription do
4
4
 
5
+ describe '#initialize builds a worker class' do
6
+ subject! do
7
+ described_class.new(source: Pet, event_name: :pooped) do
8
+ end
9
+ end
10
+
11
+ specify do
12
+ expect(Reactor::StaticSubscribers::Pet::PoopedHandler < Reactor::Workers::EventWorker)
13
+ .to be true
14
+ end
15
+
16
+ describe 'when subscriber object has namespace of arbitrary length' do
17
+ subject! do
18
+ described_class.new(source: Reactor::Subscription, event_name: :pooped) do
19
+ end
20
+ end
21
+
22
+ specify do
23
+ expect(Reactor::StaticSubscribers::Reactor::Subscription::PoopedHandler <
24
+ Reactor::Workers::EventWorker)
25
+ .to be true
26
+ end
27
+ end
28
+ end
29
+
5
30
  describe '.build_handler_name' do
6
31
  let(:event_name) { :kitten_sleeping }
7
32
 
@@ -24,7 +24,7 @@ shared_examples_for 'configurable subscriber worker' do
24
24
  end
25
25
  end
26
26
 
27
- context 'for async workers' do
27
+ context 'for not delayed workers' do
28
28
  let(:klass) { MyEventWorker }
29
29
  subject { klass.perform_where_needed(event_data) }
30
30
 
@@ -33,15 +33,5 @@ shared_examples_for 'configurable subscriber worker' do
33
33
  subject
34
34
  end
35
35
  end
36
-
37
- context 'for immediate workers' do
38
- let(:klass) { MyImmediateWorker }
39
- subject { klass.perform_where_needed(event_data) }
40
-
41
- it 'creates and executes new instance' do
42
- expect_any_instance_of(klass).to receive(:perform).with(event_data)
43
- subject
44
- end
45
- end
46
36
  end
47
37
  end
@@ -14,7 +14,6 @@ end
14
14
  class MyEventWorker < Reactor::Workers::EventWorker
15
15
  self.source = SourceSubscriber
16
16
  self.action = :fire_worker
17
- self.async = true
18
17
  self.delay = 0
19
18
  self.deprecated = false
20
19
  end
@@ -22,7 +21,6 @@ end
22
21
  class MyBlockWorker < Reactor::Workers::EventWorker
23
22
  self.source = SourceSubscriber
24
23
  self.action = lambda { |event| :block_ran }
25
- self.async = true
26
24
  self.delay = 0
27
25
  self.deprecated = false
28
26
  end
@@ -30,19 +28,10 @@ end
30
28
  class MyDelayedWorker < Reactor::Workers::EventWorker
31
29
  self.source = SourceSubscriber
32
30
  self.action = :fire_worker
33
- self.async = true
34
31
  self.delay = 1 # seconds
35
32
  self.deprecated = false
36
33
  end
37
34
 
38
- class MyImmediateWorker < Reactor::Workers::EventWorker
39
- self.source = SourceSubscriber
40
- self.action = :fire_worker
41
- self.async = false
42
- self.delay = 0
43
- self.deprecated = false
44
- end
45
-
46
35
  describe Reactor::Workers::EventWorker do
47
36
  let(:event_name) { :fire_worker }
48
37
  let(:event_data) { Hash[my_event_data: true] }
@@ -61,25 +50,15 @@ describe Reactor::Workers::EventWorker do
61
50
  end
62
51
  end
63
52
 
64
- context 'when should_perform? is false' do
65
- let(:klass) { MyEventWorker }
53
+ before { allow_any_instance_of(klass).to receive(:should_perform?).and_return(true) }
66
54
 
67
- it 'returns :__perform_aborted__' do
68
- expect(subject).to eq(:__perform_aborted__)
69
- end
55
+ it 'calls class method by symbol' do
56
+ expect(subject).to eq(:method_called)
70
57
  end
71
58
 
72
- context 'when should_perform? is true' do
73
- before { allow_any_instance_of(klass).to receive(:should_perform?).and_return(true) }
74
-
75
- it 'calls class method by symbol' do
76
- expect(subject).to eq(:method_called)
77
- end
78
-
79
- context 'for block workers' do
80
- let(:klass) { MyBlockWorker }
81
- it { is_expected.to eq(:block_ran) }
82
- end
59
+ context 'for block workers' do
60
+ let(:klass) { MyBlockWorker }
61
+ it { is_expected.to eq(:block_ran) }
83
62
  end
84
63
  end
85
64
 
@@ -14,14 +14,12 @@ end
14
14
  class MyMailerWorker < Reactor::Workers::MailerWorker
15
15
  self.source = MailerSubscriber
16
16
  self.action = :fire_mailer
17
- self.async = false
18
17
  self.delay = 0
19
18
  self.deprecated = false
20
19
  end
21
20
 
22
21
  class MyBlockMailerWorker < Reactor::Workers::MailerWorker
23
22
  self.source = MailerSubscriber
24
- self.async = false
25
23
  self.delay = 0
26
24
  self.action = lambda { |event| fire_mailer(event) }
27
25
  self.deprecated = false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - winfred
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2018-01-25 00:00:00.000000000 Z
14
+ date: 2018-03-07 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rails
@@ -205,16 +205,6 @@ files:
205
205
  - gemfiles/sidekiq_4.gemfile
206
206
  - gemfiles/sidekiq_5.gemfile
207
207
  - lib/reactor.rb
208
- - lib/reactor/controllers.rb
209
- - lib/reactor/controllers/concerns/actions/action_event.rb
210
- - lib/reactor/controllers/concerns/actions/create_event.rb
211
- - lib/reactor/controllers/concerns/actions/destroy_event.rb
212
- - lib/reactor/controllers/concerns/actions/edit_event.rb
213
- - lib/reactor/controllers/concerns/actions/index_event.rb
214
- - lib/reactor/controllers/concerns/actions/new_event.rb
215
- - lib/reactor/controllers/concerns/actions/show_event.rb
216
- - lib/reactor/controllers/concerns/actions/update_event.rb
217
- - lib/reactor/controllers/concerns/resource_actionable.rb
218
208
  - lib/reactor/errors.rb
219
209
  - lib/reactor/event.rb
220
210
  - lib/reactor/models.rb
@@ -222,19 +212,17 @@ files:
222
212
  - lib/reactor/models/concerns/subscribable.rb
223
213
  - lib/reactor/static_subscribers.rb
224
214
  - lib/reactor/subscription.rb
225
- - lib/reactor/testing.rb
226
215
  - lib/reactor/testing/matchers.rb
216
+ - lib/reactor/testing/stubs.rb
227
217
  - lib/reactor/version.rb
228
218
  - lib/reactor/workers.rb
229
219
  - lib/reactor/workers/concerns/configuration.rb
230
220
  - lib/reactor/workers/event_worker.rb
231
221
  - lib/reactor/workers/mailer_worker.rb
232
222
  - reactor.gemspec
233
- - spec/controllers/concerns/resource_actionable_spec.rb
234
223
  - spec/event_spec.rb
235
224
  - spec/models/concerns/publishable_spec.rb
236
225
  - spec/models/concerns/subscribable_spec.rb
237
- - spec/reactor_spec.rb
238
226
  - spec/spec_helper.rb
239
227
  - spec/subscription_spec.rb
240
228
  - spec/support/active_record.rb
@@ -266,11 +254,9 @@ signing_key:
266
254
  specification_version: 4
267
255
  summary: Sidekiq/ActiveRecord pubsub lib
268
256
  test_files:
269
- - spec/controllers/concerns/resource_actionable_spec.rb
270
257
  - spec/event_spec.rb
271
258
  - spec/models/concerns/publishable_spec.rb
272
259
  - spec/models/concerns/subscribable_spec.rb
273
- - spec/reactor_spec.rb
274
260
  - spec/spec_helper.rb
275
261
  - spec/subscription_spec.rb
276
262
  - spec/support/active_record.rb
@@ -1,2 +0,0 @@
1
- # FIXME - these are not properly namespaced. The directories mean nothing
2
- require "reactor/controllers/concerns/resource_actionable"
@@ -1,13 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class ActionEvent
4
- def self.perform(&block)
5
- @perform_block = block
6
- end
7
-
8
- def self.perform_on(ctx)
9
- ctx.instance_exec(&@perform_block)
10
- end
11
- end
12
- end
13
- end
@@ -1,18 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class CreateEvent < ActionEvent
4
- perform do
5
- if actionable_resource.valid?
6
- action_event "#{resource_name}_created",
7
- target: actionable_resource,
8
- attributes: params[resource_name]
9
- else
10
- action_event "#{resource_name}_create_failed",
11
- errors: actionable_resource.errors.as_json,
12
- attributes: params[resource_name],
13
- target: nested_resource
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,9 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class DestroyEvent < ActionEvent
4
- perform do
5
- action_event "#{resource_name}_destroyed", last_snapshot: actionable_resource.as_json
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class EditEvent < ActionEvent
4
- perform do
5
- action_event "edit_#{resource_name}_form_viewed", target: actionable_resource
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class IndexEvent < ActionEvent
4
- perform do
5
- action_event "#{resource_name.pluralize}_indexed", target: nested_resource
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class NewEvent < ActionEvent
4
- perform do
5
- action_event "new_#{resource_name}_form_viewed", target: nested_resource
6
- end
7
- end
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class ShowEvent < ActionEvent
4
- perform do
5
- action_event "#{resource_name}_viewed", target: actionable_resource
6
- end
7
- end
8
- end
9
- end
@@ -1,18 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- class UpdateEvent < ActionEvent
4
- perform do
5
- if actionable_resource.valid?
6
- action_event "#{resource_name}_updated",
7
- target: actionable_resource,
8
- changes: actionable_resource.previous_changes.as_json
9
- else
10
- action_event "#{resource_name}_update_failed",
11
- target: actionable_resource,
12
- errors: actionable_resource.errors.as_json,
13
- attributes: params[resource_name]
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,55 +0,0 @@
1
- module Reactor
2
- module ResourceActionable
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- around_filter :infer_basic_action_event
7
- end
8
-
9
- def infer_basic_action_event
10
- yield if block_given?
11
-
12
- if (event_descriptor = "Reactor::ResourceActionable::#{action_name.camelize}Event".safe_constantize).present?
13
- event_descriptor.perform_on self
14
- else
15
- action_event "#{resource_name}_#{action_name}"
16
- end
17
- end
18
-
19
- module ClassMethods
20
- def actionable_resource(ivar_name = nil)
21
- @resource_ivar_name ||= ivar_name
22
- end
23
-
24
- def nested_resource(ivar_name = nil)
25
- @nested_resource_ivar_name ||= ivar_name
26
- end
27
-
28
- # this is so our API controller subclasses can re-use the resource declarations
29
- def inherited(subclass)
30
- [:resource_ivar_name, :nested_resource_ivar_name].each do |inheritable_attribute|
31
- instance_var = "@#{inheritable_attribute}"
32
- subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
33
- end
34
- end
35
- end
36
-
37
- def actionable_resource; instance_variable_get(self.class.actionable_resource); end
38
- def nested_resource; self.class.nested_resource && instance_variable_get(self.class.nested_resource); end
39
-
40
- private
41
-
42
- def resource_name
43
- self.class.actionable_resource.to_s.gsub('@','').underscore
44
- end
45
- end
46
- end
47
-
48
- require "reactor/controllers/concerns/actions/action_event"
49
- require "reactor/controllers/concerns/actions/new_event"
50
- require "reactor/controllers/concerns/actions/index_event"
51
- require "reactor/controllers/concerns/actions/edit_event"
52
- require "reactor/controllers/concerns/actions/create_event"
53
- require "reactor/controllers/concerns/actions/update_event"
54
- require "reactor/controllers/concerns/actions/destroy_event"
55
- require "reactor/controllers/concerns/actions/show_event"
@@ -1,50 +0,0 @@
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,140 +0,0 @@
1
- require 'spec_helper'
2
-
3
- class RandomActionController
4
-
5
- def self.around_filter(method)
6
- @around_filter ||= method
7
- end
8
-
9
- include Reactor::ResourceActionable
10
- actionable_resource :@cat
11
- nested_resource :@owner
12
-
13
- attr_accessor :action_name
14
- def initialize
15
- self.action_name = 'create'
16
- end
17
-
18
- def create
19
- #because I dont feel like re-implementing around_filter for this stub
20
- infer_basic_action_event do
21
- @owner = ArbitraryModel.create!
22
- @cat = Pet.create!
23
- end
24
- end
25
-
26
- end
27
-
28
- describe Reactor::ResourceActionable do
29
- let(:controller_stub) { RandomActionController.new }
30
-
31
- describe "when action strategy class exists" do
32
- it 'runs the strategy of the matching name' do
33
- expect(Reactor::ResourceActionable::CreateEvent).to receive(:perform_on).with(controller_stub)
34
- controller_stub.create
35
- end
36
- end
37
-
38
- describe "when action is non-standard rails CRUD action" do
39
- it 'fires a basic action_event' do
40
- controller_stub.action_name = 'do_thing'
41
- expect(controller_stub).to receive(:action_event).with("cat_do_thing")
42
- controller_stub.create
43
- end
44
- end
45
- end
46
-
47
- describe "ActionEvents" do
48
- let(:actionable_resource) { ArbitraryModel.create! }
49
- let(:nested_resource) { Pet.create! }
50
- let(:ctrl_stub) { double(resource_name: "cat", actionable_resource: actionable_resource, nested_resource: nested_resource, params: {'cat' => {name: "Sasha"}} ) }
51
-
52
- describe "ShowEvent" do
53
- after { Reactor::ResourceActionable::ShowEvent.perform_on(ctrl_stub) }
54
- specify { expect(ctrl_stub).to receive(:action_event).with("cat_viewed", target: actionable_resource) }
55
- end
56
-
57
- describe "EditEvent" do
58
- after { Reactor::ResourceActionable::EditEvent.perform_on(ctrl_stub) }
59
- specify { expect(ctrl_stub).to receive(:action_event).with("edit_cat_form_viewed", target: actionable_resource) }
60
- end
61
-
62
- describe "NewEvent" do
63
- after { Reactor::ResourceActionable::NewEvent.perform_on(ctrl_stub) }
64
- specify { expect(ctrl_stub).to receive(:action_event).with("new_cat_form_viewed", target: nested_resource) }
65
- end
66
-
67
- describe "IndexEvent" do
68
- after { Reactor::ResourceActionable::IndexEvent.perform_on(ctrl_stub) }
69
- specify { expect(ctrl_stub).to receive(:action_event).with("cats_indexed", target: nested_resource) }
70
- end
71
-
72
- describe "DestroyEvent" do
73
- after { Reactor::ResourceActionable::DestroyEvent.perform_on(ctrl_stub) }
74
- specify { expect(ctrl_stub).to receive(:action_event).with("cat_destroyed", last_snapshot: actionable_resource.as_json) }
75
- end
76
-
77
- describe "CreateEvent" do
78
- after { Reactor::ResourceActionable::CreateEvent.perform_on(ctrl_stub) }
79
-
80
- describe "when resource is valid" do
81
- before { expect(actionable_resource).to receive(:valid?).and_return(true) }
82
-
83
- specify do
84
- expect(ctrl_stub).to receive(:action_event)
85
- .with("cat_created",
86
- target: actionable_resource,
87
- attributes: {name: "Sasha"})
88
- end
89
- end
90
-
91
- describe "when resource is not valid" do
92
- before do
93
- expect(actionable_resource).to receive(:valid?).and_return(false)
94
- expect(actionable_resource).to receive(:errors).and_return('awesomeness' => 'too awesome')
95
- end
96
-
97
- specify do
98
- expect(ctrl_stub).to receive(:action_event)
99
- .with("cat_create_failed",
100
- errors: {'awesomeness' => 'too awesome'},
101
- target: nested_resource,
102
- attributes: {name: "Sasha"})
103
- end
104
- end
105
- end
106
-
107
- describe "UpdateEvent" do
108
- after { Reactor::ResourceActionable::UpdateEvent.perform_on(ctrl_stub) }
109
-
110
- describe "when resource is valid" do
111
- before do
112
- expect(actionable_resource).to receive(:valid?).and_return(true)
113
- expect(actionable_resource).to receive(:previous_changes).and_return({'name' => [nil, "Sasha"]})
114
- end
115
-
116
- specify do
117
- expect(ctrl_stub).to receive(:action_event)
118
- .with("cat_updated",
119
- target: actionable_resource,
120
- changes: {'name' => [nil, "Sasha"]})
121
- end
122
- end
123
-
124
- describe "when resource is not valid" do
125
- before do
126
- expect(actionable_resource).to receive(:valid?).and_return(false)
127
- expect(actionable_resource).to receive(:errors).and_return('awesomeness' => 'too awesome')
128
- end
129
-
130
- specify do
131
- expect(ctrl_stub).to receive(:action_event)
132
- .with("cat_update_failed",
133
- target: actionable_resource,
134
- errors: {'awesomeness' => 'too awesome'},
135
- attributes: {name: "Sasha"})
136
- end
137
- end
138
- end
139
-
140
- end
data/spec/reactor_spec.rb DELETED
@@ -1,59 +0,0 @@
1
- require 'spec_helper'
2
-
3
- Reactor.in_test_mode do
4
- class SomeClass
5
- include Reactor::Subscribable
6
- on_event :test_event, -> (event) { self.spy_on_me }
7
- end
8
- end
9
-
10
- describe Reactor do
11
- let(:subscriber) { SomeClass }
12
-
13
- describe '.test_mode!' do
14
- before { Reactor.disable_test_mode! }
15
-
16
- it 'sets Reactor into test mode' do
17
- expect(Reactor.test_mode?).to be_falsey
18
- Reactor.test_mode!
19
- expect(Reactor.test_mode?).to be_truthy
20
- end
21
- end
22
-
23
- context 'in test mode' do
24
- before { Reactor.test_mode! }
25
- after { Reactor.disable_test_mode! }
26
-
27
- it 'subscribers created in test mode are disabled' do
28
- expect(subscriber).not_to receive :spy_on_me
29
- Reactor::Event.publish :test_event
30
- end
31
-
32
- describe '.with_subscriber_enabled' do
33
- it 'enables a subscriber during test mode' do
34
- expect(subscriber).to receive :spy_on_me
35
- Reactor.with_subscriber_enabled(subscriber) do
36
- Reactor::Event.publish :test_event
37
- end
38
- end
39
-
40
- it 'disables the subscriber outside the block' do
41
- expect(Reactor::TEST_MODE_SUBSCRIBERS).to be_empty
42
- Reactor.with_subscriber_enabled(subscriber) do
43
- expect(Reactor::TEST_MODE_SUBSCRIBERS).to contain_exactly(subscriber)
44
- end
45
- expect(Reactor::TEST_MODE_SUBSCRIBERS).to be_empty
46
- end
47
-
48
- it 'correctly handles exceptions inside the block' do
49
- expect(Reactor::TEST_MODE_SUBSCRIBERS).to be_empty
50
- expect {
51
- Reactor.with_subscriber_enabled(subscriber) do
52
- raise RuntimeError
53
- end
54
- }.to raise_error(RuntimeError)
55
- expect(Reactor::TEST_MODE_SUBSCRIBERS).to be_empty
56
- end
57
- end
58
- end
59
- end