reactor 0.6.2 → 0.7.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: 0abc4277ddd966a53c908760c99f67e32c4753e9
4
- data.tar.gz: a5990f202fda7c2987d66c68d738bc126f568f75
3
+ metadata.gz: fde04277711d635f472dbd5f930d8d229439977f
4
+ data.tar.gz: defa897759a608c46676c26ba1552887d24fb0e7
5
5
  SHA512:
6
- metadata.gz: 1d8af444ef13fc7e8e142b3392d04a841a340ef84ef5a2155a882fbedf30f1d2b94b90f90b76579b33ec7390fb9ee3bffc0aeccdc00ab0db8fd12aa8173feb07
7
- data.tar.gz: 788bb1bda4586fbcdd2d6c6f19da8de2cc5d17f161b33517beb7459f7f9b2e9b26f7a5557acdf3017ce3a1d377f4ccd2e7c1fa7322ae2300d8fcfa25b089cbf8
6
+ metadata.gz: 2a461f780da7909000cf0b9487f63a731bdbef965d2329cc8cfb7b7129fb0f0adcb16c74f4e297ed4fcbc33ccdcef675bc0b682f147c93128b74712ffd09f807
7
+ data.tar.gz: 35616c16d05954d319bc4fa55029d5432b1444332aa3ede44fc9bc8e97fb64bafbb3bc32587c3b0e66a61f0a3867a445445a95cb6024ec9426ec24ea540bed4f
data/.gitignore CHANGED
@@ -19,3 +19,4 @@ tags
19
19
  .rvmrc
20
20
  .ruby-version
21
21
  .ruby-gemset
22
+ .idea
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ services:
5
+ - redis-server
data/Gemfile CHANGED
@@ -2,9 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in reactor.gemspec
4
4
  gemspec
5
-
6
- group :test do
7
- gem 'pry'
8
- gem 'rspec'
9
- gem 'sqlite3'
10
- end
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # Reactor
1
+ # reactor.gem
2
2
 
3
- Warning: this is under active development!
3
+ ### A Sidekiq-backed pub/sub layer for your Rails app.
4
+
5
+ [![Build Status](https://travis-ci.org/hired/reactor.svg?branch=master)](https://travis-ci.org/hired/reactor)
4
6
 
5
7
  This gem aims to provide the following tools to augment your ActiveRecord & Sidekiq stack.
6
8
 
@@ -31,7 +33,9 @@ Well, this is evolving, so it's probably best to go read the specs.
31
33
 
32
34
  ### Barebones API
33
35
 
34
- Event.publish(:event_name, any: 'data', you: 'want')
36
+ ```ruby
37
+ Reactor::Event.publish(:event_name, any: 'data', you: 'want')
38
+ ```
35
39
 
36
40
  ### ActiveModel extensions
37
41
 
@@ -39,24 +43,174 @@ Well, this is evolving, so it's probably best to go read the specs.
39
43
 
40
44
  Describe lifecycle events like so
41
45
 
42
- publishes :my_model_created
43
- publishes :state_has_changed, if: -> { state_has_changed? }
46
+ ```ruby
47
+ publishes :my_model_created
48
+ publishes :state_has_changed, if: -> { state_has_changed? }
49
+ ```
44
50
 
45
51
  #### Subscribable
46
52
 
47
53
  You can now bind any block to an event in your models like so
48
54
 
49
- on_event :any_event do |event|
50
- event.target.do_something_about_it!
51
- end
55
+ ```ruby
56
+ on_event :any_event do |event|
57
+ event.target.do_something_about_it!
58
+ end
59
+ ```
52
60
 
53
61
  Static subscribers like these are automatically placed into Sidekiq and executed in the background
54
62
 
55
63
  It's also possible to run a subscriber block in memory like so
56
64
 
57
- on_event :any_event, in_memory: true do |event|
58
- event.target.do_something_about_it_and_make_the_user_wait!
65
+ ```ruby
66
+ on_event :any_event, in_memory: true do |event|
67
+ event.target.do_something_about_it_and_make_the_user_wait!
68
+ end
69
+ ```
70
+
71
+ #### ResourceActionable
72
+
73
+ Enforce a strict 1:1 match between your event model and database model with this controller mixin.
74
+
75
+
76
+ ```ruby
77
+ class PetsController < ApplicationController
78
+ include Reactor::ResourceActionable
79
+ actionable_resource :@pet
80
+
81
+ # GET /pets
82
+ # GET /pets.json
83
+ def index
84
+ @pets = current_user.pets
85
+
86
+ respond_to do |format|
87
+ format.html # index.html.erb
88
+ format.json { render json: @pets }
59
89
  end
90
+ end
91
+
92
+ def show
93
+ @pet = current_user.pets.find(params[:id])
94
+ respond_to do |format|
95
+ format.html # index.html.erb
96
+ format.json { render json: @pet }
97
+ end
98
+ end
99
+ end
100
+
101
+ ```
102
+
103
+ 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.
104
+
105
+ *Important* Reactor::ResourceActionable has one major usage constraints:
106
+
107
+ Your controller *must* have a method called "action_event" with this signature.
108
+ ```ruby
109
+ def action_event(name, options = {})
110
+ # Here's what ours looks like, but yours may look different.
111
+ actor = options[:actor] || current_user
112
+ actor.publish(name, options.merge(default_action_parameters))
113
+ #where default_action_parameters includes things like ip_address, referrer, user_agent
114
+ end
115
+ ```
116
+
117
+ 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):
118
+
119
+ <dl>
120
+ <dt>index =></dt>
121
+ <dd>"pets_indexed"</dd>
122
+ </dl>
123
+
124
+ <dl>
125
+ <dt>show =></dt>
126
+ <dd>"pet_viewed", target: @pet</dd>
127
+ </dl>
128
+
129
+ <dl>
130
+ <dt>new =></dt>
131
+ <dd>"new_pet_form_viewed"</dd>
132
+ </dl>
133
+
134
+ <dl>
135
+ <dt>edit =></dt>
136
+ <dd> "edit_pet_form_viewed", target: @pet</dd>
137
+ </dl>
138
+
139
+ <dl>
140
+ <dt>create =></dt>
141
+ <dd> when valid => "pet_created", target: @pet, attributes: params[:pet]
142
+ <br />
143
+ when invalid => "pet_create_failed", errors: @pet.errors, attributes: params[:pet]</dd>
144
+ </dl>
145
+
146
+ <dl>
147
+ <dt>update =></dt>
148
+ <dd>
149
+ when valid => "pet_updated", target: @pet, changes: @pet.previous_changes.as_json
150
+ <br />
151
+ when invalid => "pet_update_failed", target: @pet,
152
+ errors: @pet.errors.as_json, attributes: params[:pet]
153
+ </dd>
154
+ </dl>
155
+
156
+ <dl>
157
+ <dt>destroy =></dt>
158
+ <dd>"pet_destroyed", last_snapshot: @pet.as_jsont</dd>
159
+ </dl>
160
+
161
+
162
+ ##### What for?
163
+
164
+ 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
165
+ * determine which form field validations are constantly being hit by users
166
+ * see if there are any fields that are consistently ignored on that form until later
167
+ * recover data from the last_snapshot of a destroyed record
168
+ * write a small conversion funnel analysis to see who never makes it back to a record to update it
169
+ * 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)
170
+
171
+ For example, in an action mailer.
172
+
173
+ ```ruby
174
+ class MyMailer < ActionMailer::Base
175
+ include Reactor::EventMailer
176
+
177
+ on_event :pet_created do |event|
178
+ @user = event.actor
179
+ @pet = event.target
180
+ mail to: @user.email, subject: "Your pet is already hungry!", body: "feed it."
181
+ end
182
+ end
183
+ ```
184
+
185
+ Or in a model, concern, or other business logic file.
186
+
187
+ ```ruby
188
+ class MyClass
189
+ include Reactor::Subscribable
190
+
191
+ on_event :pet_updated do |event|
192
+ event.actor.recalculate_expensive_something_for(event.target)
193
+ end
194
+ end
195
+ ```
196
+
197
+ ### Testing
198
+
199
+ Calling `Reactor.test_mode!` enables test mode. (You should call this as early as possible, before your subscriber classes
200
+ are declared). In test mode, no subscribers will fire unless they are specifically enabled, which can be accomplished
201
+ by calling
202
+ ```ruby
203
+ Reactor.enable_test_mode_subscriber(MyAwesomeSubscriberClass)
204
+ ```
205
+
206
+ We also provide
207
+ ```ruby
208
+ Reactor.with_subscriber_enabled(MyClass) do
209
+ # stuff
210
+ end
211
+ ```
212
+
213
+ for your testing convenience.
60
214
 
61
215
  ## Contributing
62
216
 
@@ -65,3 +219,17 @@ Well, this is evolving, so it's probably best to go read the specs.
65
219
  3. Commit your changes (`git commit -am 'Add some feature'`)
66
220
  4. Push to the branch (`git push origin my-new-feature`)
67
221
  5. Create new Pull Request
222
+
223
+ ## Open Source by Hired
224
+
225
+ [Hired](https://hired.com/?utm_source=opensource&utm_medium=reactor&utm_campaign=readme) wants to make sure every developer in the world has a kick-ass job with an awesome salary and great coworkers.
226
+
227
+ Our site allows you to quickly create a profile and then get offers from some of the top companies in the world - with salary and equity disclosed up-front. Average Ruby engineer salaries on Hired are around $120,000 per year, but if you are smart enough to use Reactor you'll probably be able to get more like $150,000 :).
228
+
229
+
230
+ <a href="https://hired.com/?utm_source=opensource&utm_medium=reactor&utm_campaign=readme-banner" target="_blank">
231
+ <img src="https://dmrxx81gnj0ct.cloudfront.net/public/hired-banner-light-1-728x90.png" alt="Hired" width="728" height="90" align="center"/>
232
+ </a>
233
+
234
+ We are Ruby developers ourselves, and we use all of our open source projects in production. We always encourge forks, pull requests, and issues. Get in touch with the Hired Engineering team at _opensource@hired.com_.
235
+
data/Rakefile CHANGED
@@ -1 +1,7 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,9 @@
1
+ class Reactor::ResourceActionable::ActionEvent
2
+ def self.perform(&block)
3
+ @perform_block = block
4
+ end
5
+
6
+ def self.perform_on(ctx)
7
+ ctx.instance_exec(&@perform_block)
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ class Reactor::ResourceActionable::CreateEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ if actionable_resource.valid?
4
+ action_event "#{resource_name}_created",
5
+ target: actionable_resource,
6
+ attributes: params[resource_name]
7
+ else
8
+ action_event "#{resource_name}_create_failed",
9
+ errors: actionable_resource.errors.as_json,
10
+ attributes: params[resource_name],
11
+ target: nested_resource
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ class Reactor::ResourceActionable::DestroyEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ action_event "#{resource_name}_destroyed", last_snapshot: actionable_resource.as_json
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Reactor::ResourceActionable::EditEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ action_event "edit_#{resource_name}_form_viewed", target: actionable_resource
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Reactor::ResourceActionable::IndexEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ action_event "#{resource_name.pluralize}_indexed", target: nested_resource
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Reactor::ResourceActionable::NewEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ action_event "new_#{resource_name}_form_viewed", target: nested_resource
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Reactor::ResourceActionable::ShowEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ action_event "#{resource_name}_viewed", target: actionable_resource
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ class Reactor::ResourceActionable::UpdateEvent < Reactor::ResourceActionable::ActionEvent
2
+ perform do
3
+ if actionable_resource.valid?
4
+ action_event "#{resource_name}_updated",
5
+ target: actionable_resource,
6
+ changes: actionable_resource.previous_changes.as_json
7
+ else
8
+ action_event "#{resource_name}_update_failed",
9
+ target: actionable_resource,
10
+ errors: actionable_resource.errors.as_json,
11
+ attributes: params[resource_name]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ module Reactor::ResourceActionable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ around_filter :infer_basic_action_event
6
+ end
7
+
8
+ def infer_basic_action_event
9
+ yield if block_given?
10
+
11
+ if (event_descriptor = "Reactor::ResourceActionable::#{action_name.camelize}Event".safe_constantize).present?
12
+ event_descriptor.perform_on self
13
+ else
14
+ action_event "#{resource_name}_#{action_name}"
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def actionable_resource(ivar_name = nil)
20
+ @resource_ivar_name ||= ivar_name
21
+ end
22
+
23
+ def nested_resource(ivar_name = nil)
24
+ @nested_resource_ivar_name ||= ivar_name
25
+ end
26
+
27
+ # this is so our API controller subclasses can re-use the resource declarations
28
+ def inherited(subclass)
29
+ [:resource_ivar_name, :nested_resource_ivar_name].each do |inheritable_attribute|
30
+ instance_var = "@#{inheritable_attribute}"
31
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
32
+ end
33
+ end
34
+ end
35
+
36
+ def actionable_resource; instance_variable_get(self.class.actionable_resource); end
37
+ def nested_resource; self.class.nested_resource && instance_variable_get(self.class.nested_resource); end
38
+
39
+ private
40
+
41
+ def resource_name
42
+ self.class.actionable_resource.to_s.gsub('@','').underscore
43
+ end
44
+ end
45
+
46
+ require "reactor/controllers/concerns/actions/action_event"
47
+ require "reactor/controllers/concerns/actions/new_event"
48
+ require "reactor/controllers/concerns/actions/index_event"
49
+ require "reactor/controllers/concerns/actions/edit_event"
50
+ require "reactor/controllers/concerns/actions/create_event"
51
+ require "reactor/controllers/concerns/actions/update_event"
52
+ require "reactor/controllers/concerns/actions/destroy_event"
53
+ require "reactor/controllers/concerns/actions/show_event"
data/lib/reactor/event.rb CHANGED
@@ -85,13 +85,9 @@ class Reactor::Event
85
85
  end
86
86
 
87
87
  def fire_database_driven_subscribers(data, name)
88
- Reactor::Subscriber.where(event: name).each do |subscriber|
89
- Reactor::Subscriber.delay.fire subscriber.id, data
90
- end
91
-
92
88
  #TODO: support more matching?
93
- Reactor::Subscriber.where(event: '*').each do |s|
94
- Reactor::Subscriber.delay.fire s.id, data
89
+ Reactor::Subscriber.where(event_name: [name, '*']).each do |subscriber|
90
+ Reactor::Subscriber.delay.fire subscriber.id, data
95
91
  end
96
92
  end
97
93
 
@@ -2,7 +2,8 @@ module Reactor::Publishable
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- after_commit :schedule_events, if: :persisted?
5
+ after_commit :schedule_events, if: :persisted?, on: :create
6
+ after_commit :schedule_conditional_events, if: :persisted?, on: [:create, :update]
6
7
  after_commit :reschedule_events, if: :persisted?, on: :update
7
8
  end
8
9
 
@@ -24,20 +25,8 @@ module Reactor::Publishable
24
25
 
25
26
  def schedule_events
26
27
  self.class.events.each do |name, data|
27
- event = data.merge(
28
- actor: ( data[:actor] ? send(data[:actor]) : self ),
29
- target: ( data[:target] ? self : nil),
30
- at: ( data[:at] ? send(data[:at]) : nil)
31
- ).except(:watch, :if)
32
- need_to_fire = case (ifarg = data[:if])
33
- when Proc
34
- instance_exec(&ifarg)
35
- when Symbol
36
- send(ifarg)
37
- else
38
- transaction_include_action?(:create)
39
- end
40
- Reactor::Event.publish name, event if need_to_fire
28
+ event = event_data_for_signature(data)
29
+ Reactor::Event.publish name, event
41
30
  end
42
31
  end
43
32
 
@@ -55,4 +44,25 @@ module Reactor::Publishable
55
44
  end
56
45
  end
57
46
 
47
+ def schedule_conditional_events
48
+ self.class.events.select { |k,v| v.has_key?(:if) }.each do |name, data|
49
+ event = event_data_for_signature(data)
50
+ need_to_fire = case (ifarg = data[:if])
51
+ when Proc
52
+ instance_exec(&ifarg)
53
+ when Symbol
54
+ send(ifarg)
55
+ end
56
+ Reactor::Event.publish name, event if need_to_fire
57
+ end
58
+ end
59
+
60
+ def event_data_for_signature(signature)
61
+ signature.merge(
62
+ actor: (signature[:actor] ? send(signature[:actor]) : self),
63
+ target: (signature[:target] ? self : nil),
64
+ at: (signature[:at] ? send(signature[:at]) : nil)
65
+ ).except(:watch, :if)
66
+ end
67
+
58
68
  end
@@ -19,37 +19,39 @@ module Reactor::Subscribable
19
19
  i+= 1
20
20
  end while Reactor::StaticSubscribers.const_defined?(new_class)
21
21
 
22
- eval %Q{
23
- class Reactor::StaticSubscribers::#{new_class}
24
- include Sidekiq::Worker
25
-
26
- cattr_accessor :method, :delay, :source, :in_memory
27
-
28
- def perform(data)
29
- event = Reactor::Event.new(data)
30
- if @@method.is_a?(Symbol)
31
- @@source.delay_for(@@delay).send(@@method, event)
32
- else
33
- @@method.call(event)
34
- end
22
+ klass = Class.new do
23
+ include Sidekiq::Worker
24
+
25
+ class_attribute :method, :delay, :source, :in_memory, :dont_perform
26
+
27
+ def perform(data)
28
+ return :__perform_aborted__ if dont_perform && !Reactor::TEST_MODE_SUBSCRIBERS.include?(source)
29
+ event = Reactor::Event.new(data)
30
+ if method.is_a?(Symbol)
31
+ source.delay_for(delay).send(method, event)
32
+ else
33
+ method.call(event)
35
34
  end
35
+ end
36
36
 
37
- def self.perform_where_needed(data)
38
- if @@in_memory
39
- new.perform(data)
40
- else
41
- perform_async(data)
42
- end
37
+ def self.perform_where_needed(data)
38
+ if in_memory
39
+ new.perform(data)
40
+ else
41
+ perform_async(data)
43
42
  end
44
43
  end
45
- }
46
-
47
- new_class = "Reactor::StaticSubscribers::#{new_class}".constantize
48
- new_class.method = method || block
49
- new_class.delay = options[:delay] || 0
50
- new_class.source = options[:source]
51
- new_class.in_memory = options[:in_memory]
52
- new_class
44
+ end
45
+
46
+ Reactor::StaticSubscribers.const_set(new_class, klass)
47
+
48
+ klass.tap do |k|
49
+ k.method = method || block
50
+ k.delay = options[:delay] || 0
51
+ k.source = options[:source]
52
+ k.in_memory = options[:in_memory]
53
+ k.dont_perform = Reactor.test_mode?
54
+ end
53
55
  end
54
56
  end
55
- end
57
+ end
@@ -1,12 +1,12 @@
1
1
  class Reactor::Subscriber < ActiveRecord::Base
2
- attr_accessor :message
2
+ attr_accessor :event
3
3
 
4
- def event=(event)
5
- write_attribute :event, event.to_s
4
+ def event_name=(event)
5
+ write_attribute :event_name, event.to_s
6
6
  end
7
7
 
8
8
  def fire(data)
9
- self.message = Reactor::Event.new(data)
9
+ self.event = Reactor::Event.new(data)
10
10
  instance_exec &self.class.on_fire
11
11
  self
12
12
  end
@@ -22,19 +22,5 @@ class Reactor::Subscriber < ActiveRecord::Base
22
22
  def fire(subscriber_id, data)
23
23
  Reactor::Subscriber.find(subscriber_id).fire data
24
24
  end
25
-
26
- def subscribes_to(name = nil, data = {})
27
- #subscribers << name
28
- #TODO: REMEMBER SUBSCRIBERS so we can define them in code as well as with a row in the DB
29
- # until then, here's a helper to make it easy to create with random data in postgres
30
- # total crap I know but whatever
31
- define_singleton_method :exists! do
32
- chain = where(event: name)
33
- data.each do |key, value|
34
- chain = chain.where("subscribers.data @> ?", "#{key}=>#{value}")
35
- end
36
- chain.first_or_create!(data)
37
- end
38
- end
39
25
  end
40
26
  end
@@ -1,3 +1,3 @@
1
1
  module Reactor
2
- VERSION = "0.6.2"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/reactor.rb CHANGED
@@ -3,13 +3,48 @@ require "reactor/models/concerns/publishable"
3
3
  require "reactor/models/concerns/subscribable"
4
4
  require "reactor/models/concerns/optionally_subclassable"
5
5
  require "reactor/models/subscriber"
6
+ require "reactor/controllers/concerns/resource_actionable"
6
7
  require "reactor/event"
7
8
 
8
9
  module Reactor
9
10
  SUBSCRIBERS = {}
11
+ TEST_MODE_SUBSCRIBERS = Set.new
12
+ @@test_mode = false
13
+
10
14
  module StaticSubscribers
11
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
24
+
25
+ def self.disable_test_mode!
26
+ @@test_mode = false
27
+ end
28
+
29
+ def self.in_test_mode
30
+ test_mode!
31
+ (yield if block_given?).tap { disable_test_mode! }
32
+ end
33
+
34
+ def self.enable_test_mode_subscriber(klass)
35
+ TEST_MODE_SUBSCRIBERS << klass
36
+ end
37
+
38
+ def self.disable_test_mode_subscriber(klass)
39
+ TEST_MODE_SUBSCRIBERS.delete klass
40
+ end
41
+
42
+ def self.with_subscriber_enabled(klass)
43
+ enable_test_mode_subscriber klass
44
+ yield if block_given?
45
+ disable_test_mode_subscriber klass
46
+ end
12
47
  end
13
48
 
14
49
  ActiveRecord::Base.send(:include, Reactor::Publishable)
15
- ActiveRecord::Base.send(:include, Reactor::Subscribable)
50
+ ActiveRecord::Base.send(:include, Reactor::Subscribable)
data/reactor.gemspec CHANGED
@@ -6,8 +6,8 @@ require 'reactor/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "reactor"
8
8
  spec.version = Reactor::VERSION
9
- spec.authors = ["winfred", "walt", "nate", "cgag", "petermin"]
10
- spec.email = ["winfred@developerauction.com", "walt@developerauction.com", "curtis@developerauction.com", "nate@developerauction.com", "kengteh.min@gmail.com"]
9
+ spec.authors = ["winfred", "walt", "nate", "petermin"]
10
+ spec.email = ["winfred@hired.com", "walt@hired.com", "nate@hired.com", "kengteh.min@gmail.com"]
11
11
  spec.description = %q{ rails chrono reactor }
12
12
  spec.summary = %q{ Sidekiq/ActiveRecord pubsub lib }
13
13
  spec.homepage = ""
@@ -18,9 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "sidekiq", ">= 2.13.0"
22
- spec.add_dependency 'activerecord', '~> 3.2.13'
21
+ spec.add_dependency "sidekiq", "> 2.0"
22
+ spec.add_dependency 'activerecord', '> 3.0'
23
+
23
24
  spec.add_development_dependency "bundler", "~> 1.3"
24
25
  spec.add_development_dependency "rake"
25
- spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "rspec", "~> 2.14.1"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "sqlite3"
29
+ spec.add_development_dependency "test_after_commit"
26
30
  end
@@ -0,0 +1,140 @@
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
+ Reactor::ResourceActionable::CreateEvent.should_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
+ controller_stub.should_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) { stub(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 { ctrl_stub.should_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 { ctrl_stub.should_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 { ctrl_stub.should_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 { ctrl_stub.should_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 { ctrl_stub.should_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 { actionable_resource.should_receive(:valid?).and_return(true) }
82
+
83
+ specify do
84
+ ctrl_stub.should_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
+ actionable_resource.should_receive(:valid?).and_return(false)
94
+ actionable_resource.should_receive(:errors).and_return('awesomeness' => 'too awesome')
95
+ end
96
+
97
+ specify do
98
+ ctrl_stub.should_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
+ actionable_resource.should_receive(:valid?).and_return(true)
113
+ actionable_resource.should_receive(:previous_changes).and_return({'name' => [nil, "Sasha"]})
114
+ end
115
+
116
+ specify do
117
+ ctrl_stub.should_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
+ actionable_resource.should_receive(:valid?).and_return(false)
127
+ actionable_resource.should_receive(:errors).and_return('awesomeness' => 'too awesome')
128
+ end
129
+
130
+ specify do
131
+ ctrl_stub.should_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/event_spec.rb CHANGED
@@ -24,7 +24,7 @@ describe Reactor::Event do
24
24
  end
25
25
 
26
26
  describe 'perform' do
27
- before { Reactor::Subscriber.create(event: :user_did_this) }
27
+ before { Reactor::Subscriber.create(event_name: :user_did_this) }
28
28
  after { Reactor::Subscriber.destroy_all }
29
29
  it 'fires all subscribers' do
30
30
  Reactor::Subscriber.any_instance.should_receive(:fire).with(hash_including(actor_id: '1'))
@@ -1,8 +1,5 @@
1
1
  require 'spec_helper'
2
2
 
3
- class Pet < ActiveRecord::Base
4
- end
5
-
6
3
  class Auction < ActiveRecord::Base
7
4
  attr_accessor :we_want_it
8
5
  belongs_to :pet
@@ -85,7 +82,7 @@ describe Reactor::Publishable do
85
82
  end
86
83
 
87
84
  it 'supports immediate events (on create) that get fired once' do
88
- TestSubscriber.create! event: :bell
85
+ TestSubscriber.create! event_name: :bell
89
86
  auction
90
87
  TestSubscriber.class_variable_get(:@@called).should be_true
91
88
  TestSubscriber.class_variable_set(:@@called, false)
@@ -95,19 +92,20 @@ describe Reactor::Publishable do
95
92
  end
96
93
 
97
94
  it 'does not publish an event scheduled for the past' do
98
- TestSubscriber.create! event: :begin
95
+ TestSubscriber.create! event_name: :begin
99
96
  auction
100
97
  TestSubscriber.class_variable_get(:@@called).should be_false
101
98
  end
102
99
 
103
100
  it 'does publish an event scheduled for the future' do
104
- TestSubscriber.create! event: :begin
101
+ TestSubscriber.create! event_name: :begin
105
102
  Auction.create!(pet: pet, start_at: Time.current + 1.week)
103
+
106
104
  TestSubscriber.class_variable_get(:@@called).should be_true
107
105
  end
108
106
 
109
107
  it 'can fire events onsave for any condition' do
110
- TestSubscriber.create! event: :conditional_event_on_save
108
+ TestSubscriber.create! event_name: :conditional_event_on_save
111
109
  auction
112
110
  TestSubscriber.class_variable_set(:@@called, false)
113
111
  auction.start_at = 1.day.from_now
@@ -22,6 +22,12 @@ class Auction < ActiveRecord::Base
22
22
  end
23
23
  end
24
24
 
25
+ Reactor.in_test_mode do
26
+ class TestModeAuction < ActiveRecord::Base
27
+ on_event :test_puppy_delivered, -> (event) { pp "success" }
28
+ end
29
+ end
30
+
25
31
  describe Reactor::Subscribable do
26
32
  let(:scheduled) { Sidekiq::ScheduledSet.new }
27
33
 
@@ -73,5 +79,18 @@ describe Reactor::Subscribable do
73
79
  Reactor::Event.publish(:puppy_delivered)
74
80
  end
75
81
  end
82
+
83
+ describe '#perform' do
84
+ it 'returns :__perform_aborted__ when Reactor is in test mode' do
85
+ Reactor::StaticSubscribers::TestPuppyDeliveredHandler0.new.perform({}).should == :__perform_aborted__
86
+ Reactor::Event.publish(:test_puppy_delivered)
87
+ end
88
+
89
+ it 'performs normally when specifically enabled' do
90
+ Reactor.enable_test_mode_subscriber(TestModeAuction)
91
+ Reactor::StaticSubscribers::TestPuppyDeliveredHandler0.new.perform({}).should_not == :__perform_aborted__
92
+ Reactor::Event.publish(:test_puppy_delivered)
93
+ end
94
+ end
76
95
  end
77
96
  end
@@ -11,22 +11,20 @@ end
11
11
  describe Reactor::Subscriber do
12
12
 
13
13
  describe 'fire' do
14
- subject { MySubscriber.create(event: :you_name_it).fire some: 'random', event: 'data' }
14
+ subject { MySubscriber.create(event_name: :you_name_it).fire some: 'random', event: 'data' }
15
15
 
16
- its(:message) { should be_a Reactor::Event }
17
- its('message.some') { should == 'random' }
16
+ its(:event) { should be_a Reactor::Event }
17
+ its('event.some') { should == 'random' }
18
18
 
19
19
  it 'executes block given' do
20
20
  subject.was_called.should be_true
21
21
  end
22
22
  end
23
23
 
24
- describe '.subscribes_to class helper' do
25
- end
26
24
 
27
25
  describe 'matcher' do
28
26
  it 'can be set to star to bind to all events' do
29
- MySubscriber.create!(event: '*')
27
+ MySubscriber.create!(event_name: '*')
30
28
  MySubscriber.any_instance.should_receive(:fire).with(hash_including('random' => 'data', 'event' => 'this_event'))
31
29
  Reactor::Event.publish(:this_event, {random: 'data'})
32
30
  end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe Reactor do
5
+ let(:subscriber) do
6
+ Reactor.in_test_mode do
7
+ Class.new(ActiveRecord::Base) do
8
+ on_event :test_event, -> (event) { self.spy_on_me }
9
+ end
10
+ end
11
+ end
12
+
13
+ describe '.test_mode!' do
14
+ it 'sets Reactor into test mode' do
15
+ Reactor.test_mode?.should be_false
16
+ Reactor.test_mode!
17
+ Reactor.test_mode?.should be_true
18
+ end
19
+ end
20
+
21
+ context 'in test mode' do
22
+ before { Reactor.test_mode! }
23
+ after { Reactor.disable_test_mode! }
24
+
25
+ it 'subscribers created in test mode are disabled' do
26
+ subscriber.should_not_receive :spy_on_me
27
+ Reactor::Event.publish :test_event
28
+ end
29
+
30
+ describe '.with_subscriber_enabled' do
31
+ it 'enables a subscriber during test mode' do
32
+ subscriber.should_receive :spy_on_me
33
+ Reactor.with_subscriber_enabled(subscriber) do
34
+ Reactor::Event.publish :test_event
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
data/spec/spec_helper.rb CHANGED
@@ -5,6 +5,7 @@ require 'pry'
5
5
  require 'support/active_record'
6
6
  require 'sidekiq'
7
7
  require 'sidekiq/testing/inline'
8
+ require 'sidekiq/api'
8
9
  require 'reactor'
9
10
  require 'reactor/testing/matchers'
10
11
 
@@ -14,7 +14,7 @@ ActiveRecord::Migration.create_table :auctions do |t|
14
14
  end
15
15
 
16
16
  ActiveRecord::Migration.create_table :subscribers do |t|
17
- t.string :event
17
+ t.string :event_name
18
18
  t.string :type
19
19
 
20
20
  t.timestamps
@@ -33,3 +33,9 @@ ActiveRecord::Migration.create_table :arbitrary_models do |t|
33
33
 
34
34
  t.timestamps
35
35
  end
36
+
37
+ class Pet < ActiveRecord::Base
38
+ end
39
+
40
+ class ArbitraryModel < ActiveRecord::Base
41
+ end
metadata CHANGED
@@ -1,106 +1,157 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - winfred
8
8
  - walt
9
9
  - nate
10
- - cgag
11
10
  - petermin
12
11
  autorequire:
13
12
  bindir: bin
14
13
  cert_chain: []
15
- date: 2014-04-11 00:00:00.000000000 Z
14
+ date: 2014-06-20 00:00:00.000000000 Z
16
15
  dependencies:
17
16
  - !ruby/object:Gem::Dependency
18
17
  name: sidekiq
19
18
  requirement: !ruby/object:Gem::Requirement
20
19
  requirements:
21
- - - '>='
20
+ - - ">"
22
21
  - !ruby/object:Gem::Version
23
- version: 2.13.0
22
+ version: '2.0'
24
23
  type: :runtime
25
24
  prerelease: false
26
25
  version_requirements: !ruby/object:Gem::Requirement
27
26
  requirements:
28
- - - '>='
27
+ - - ">"
29
28
  - !ruby/object:Gem::Version
30
- version: 2.13.0
29
+ version: '2.0'
31
30
  - !ruby/object:Gem::Dependency
32
31
  name: activerecord
33
32
  requirement: !ruby/object:Gem::Requirement
34
33
  requirements:
35
- - - ~>
34
+ - - ">"
36
35
  - !ruby/object:Gem::Version
37
- version: 3.2.13
36
+ version: '3.0'
38
37
  type: :runtime
39
38
  prerelease: false
40
39
  version_requirements: !ruby/object:Gem::Requirement
41
40
  requirements:
42
- - - ~>
41
+ - - ">"
43
42
  - !ruby/object:Gem::Version
44
- version: 3.2.13
43
+ version: '3.0'
45
44
  - !ruby/object:Gem::Dependency
46
45
  name: bundler
47
46
  requirement: !ruby/object:Gem::Requirement
48
47
  requirements:
49
- - - ~>
48
+ - - "~>"
50
49
  - !ruby/object:Gem::Version
51
50
  version: '1.3'
52
51
  type: :development
53
52
  prerelease: false
54
53
  version_requirements: !ruby/object:Gem::Requirement
55
54
  requirements:
56
- - - ~>
55
+ - - "~>"
57
56
  - !ruby/object:Gem::Version
58
57
  version: '1.3'
59
58
  - !ruby/object:Gem::Dependency
60
59
  name: rake
61
60
  requirement: !ruby/object:Gem::Requirement
62
61
  requirements:
63
- - - '>='
62
+ - - ">="
64
63
  - !ruby/object:Gem::Version
65
64
  version: '0'
66
65
  type: :development
67
66
  prerelease: false
68
67
  version_requirements: !ruby/object:Gem::Requirement
69
68
  requirements:
70
- - - '>='
69
+ - - ">="
71
70
  - !ruby/object:Gem::Version
72
71
  version: '0'
73
72
  - !ruby/object:Gem::Dependency
74
73
  name: rspec
75
74
  requirement: !ruby/object:Gem::Requirement
76
75
  requirements:
77
- - - '>='
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: 2.14.1
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: 2.14.1
86
+ - !ruby/object:Gem::Dependency
87
+ name: pry
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: sqlite3
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ - !ruby/object:Gem::Dependency
115
+ name: test_after_commit
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
78
119
  - !ruby/object:Gem::Version
79
120
  version: '0'
80
121
  type: :development
81
122
  prerelease: false
82
123
  version_requirements: !ruby/object:Gem::Requirement
83
124
  requirements:
84
- - - '>='
125
+ - - ">="
85
126
  - !ruby/object:Gem::Version
86
127
  version: '0'
87
- description: ' rails chrono reactor '
128
+ description: " rails chrono reactor "
88
129
  email:
89
- - winfred@developerauction.com
90
- - walt@developerauction.com
91
- - curtis@developerauction.com
92
- - nate@developerauction.com
130
+ - winfred@hired.com
131
+ - walt@hired.com
132
+ - nate@hired.com
93
133
  - kengteh.min@gmail.com
94
134
  executables: []
95
135
  extensions: []
96
136
  extra_rdoc_files: []
97
137
  files:
98
- - .gitignore
138
+ - ".gitignore"
139
+ - ".rspec"
140
+ - ".travis.yml"
99
141
  - Gemfile
100
142
  - LICENSE.txt
101
143
  - README.md
102
144
  - Rakefile
103
145
  - lib/reactor.rb
146
+ - lib/reactor/controllers/concerns/actions/action_event.rb
147
+ - lib/reactor/controllers/concerns/actions/create_event.rb
148
+ - lib/reactor/controllers/concerns/actions/destroy_event.rb
149
+ - lib/reactor/controllers/concerns/actions/edit_event.rb
150
+ - lib/reactor/controllers/concerns/actions/index_event.rb
151
+ - lib/reactor/controllers/concerns/actions/new_event.rb
152
+ - lib/reactor/controllers/concerns/actions/show_event.rb
153
+ - lib/reactor/controllers/concerns/actions/update_event.rb
154
+ - lib/reactor/controllers/concerns/resource_actionable.rb
104
155
  - lib/reactor/event.rb
105
156
  - lib/reactor/models/concerns/optionally_subclassable.rb
106
157
  - lib/reactor/models/concerns/publishable.rb
@@ -109,10 +160,12 @@ files:
109
160
  - lib/reactor/testing/matchers.rb
110
161
  - lib/reactor/version.rb
111
162
  - reactor.gemspec
163
+ - spec/controllers/concerns/resource_actionable_spec.rb
112
164
  - spec/event_spec.rb
113
165
  - spec/models/concerns/publishable_spec.rb
114
166
  - spec/models/concerns/subscribable_spec.rb
115
167
  - spec/models/subscriber_spec.rb
168
+ - spec/reactor_spec.rb
116
169
  - spec/spec_helper.rb
117
170
  - spec/support/active_record.rb
118
171
  homepage: ''
@@ -125,24 +178,26 @@ require_paths:
125
178
  - lib
126
179
  required_ruby_version: !ruby/object:Gem::Requirement
127
180
  requirements:
128
- - - '>='
181
+ - - ">="
129
182
  - !ruby/object:Gem::Version
130
183
  version: '0'
131
184
  required_rubygems_version: !ruby/object:Gem::Requirement
132
185
  requirements:
133
- - - '>='
186
+ - - ">="
134
187
  - !ruby/object:Gem::Version
135
188
  version: '0'
136
189
  requirements: []
137
190
  rubyforge_project:
138
- rubygems_version: 2.1.11
191
+ rubygems_version: 2.2.2
139
192
  signing_key:
140
193
  specification_version: 4
141
194
  summary: Sidekiq/ActiveRecord pubsub lib
142
195
  test_files:
196
+ - spec/controllers/concerns/resource_actionable_spec.rb
143
197
  - spec/event_spec.rb
144
198
  - spec/models/concerns/publishable_spec.rb
145
199
  - spec/models/concerns/subscribable_spec.rb
146
200
  - spec/models/subscriber_spec.rb
201
+ - spec/reactor_spec.rb
147
202
  - spec/spec_helper.rb
148
203
  - spec/support/active_record.rb