wisper 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +6 -1
- data/README.md +100 -152
- data/lib/wisper.rb +24 -5
- data/lib/wisper/configuration.rb +2 -0
- data/lib/wisper/global_listeners.rb +16 -11
- data/lib/wisper/publisher.rb +20 -9
- data/lib/wisper/temporary_listeners.rb +8 -13
- data/lib/wisper/version.rb +1 -1
- data/spec/lib/global_subscribers_spec.rb +11 -12
- data/spec/lib/integration_spec.rb +3 -16
- data/spec/lib/simple_example_spec.rb +1 -1
- data/spec/lib/temporary_global_listeners_spec.rb +8 -4
- data/spec/lib/wisper/publisher_spec.rb +94 -52
- data/spec/lib/wisper_spec.rb +10 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c7ce841a6cb6f5ea98bb85ff8c61b74ed007107
|
4
|
+
data.tar.gz: 57fd4d7de64134f55724e6bbf152b092312383e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 37ab485a83aa173857edca6d8fd76911f4cda61bccf6fb3af5adb47e2503dd2a9fe182be75227d3f970d15e64933af16920319d2d47911e5e8d444d18df8ff55
|
7
|
+
data.tar.gz: 8cf79f4c35a792c87c97fc83fa6dcac78bfb60a70c3e80b6f052148b31a87618b4538b165ca71ef0a9b820f38bf3ec5f413055fbd0eaef36e0bd44d482ba694f
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
# Wisper
|
2
2
|
|
3
|
-
|
4
|
-
Ruby objects using Pub/Sub.
|
3
|
+
*A micro library providing Ruby objects with Publish-Subscribe capabilities*
|
5
4
|
|
6
5
|
[](http://badge.fury.io/rb/wisper)
|
7
6
|
[](https://codeclimate.com/github/krisleech/wisper)
|
8
7
|
[](https://travis-ci.org/krisleech/wisper)
|
9
8
|
[](https://coveralls.io/r/krisleech/wisper?branch=master)
|
10
9
|
|
11
|
-
|
10
|
+
* Decouple core business logic from external concerns in Hexagonal style architectures
|
11
|
+
* Use as an alternative to ActiveRecord callbacks and Observers in Rails apps
|
12
|
+
* Connect objects based on context without permanence
|
13
|
+
* React to events synchronously or asynchronously
|
12
14
|
|
13
|
-
|
14
|
-
to reduce coupling between data and domain layers.
|
15
|
+
Note: Wisper was originally extracted from a Rails codebase but is not dependant on Rails.
|
15
16
|
|
16
17
|
## Installation
|
17
18
|
|
@@ -29,188 +30,131 @@ to subscribed listeners. Listeners subscribe, at runtime, to the publisher.
|
|
29
30
|
### Publishing
|
30
31
|
|
31
32
|
```ruby
|
32
|
-
class
|
33
|
+
class CancelOrder
|
33
34
|
include Wisper::Publisher
|
34
|
-
|
35
|
-
def
|
36
|
-
|
37
|
-
|
35
|
+
|
36
|
+
def call(order_id)
|
37
|
+
order = Order.find_by_id(order_id)
|
38
|
+
|
39
|
+
# business logic...
|
40
|
+
|
41
|
+
if order.cancelled?
|
42
|
+
broadcast(:cancel_order_successful, order.id)
|
43
|
+
else
|
44
|
+
broadcast(:cancel_order_failed, order.id)
|
45
|
+
end
|
38
46
|
end
|
39
47
|
end
|
40
48
|
```
|
41
49
|
|
42
|
-
When a publisher broadcasts an event it can
|
43
|
-
are to be passed on to the listeners.
|
50
|
+
When a publisher broadcasts an event it can include number of arguments.
|
44
51
|
|
45
|
-
|
46
|
-
|
47
|
-
|
52
|
+
The `broadcast` method is also aliased as `publish` and `announce`.
|
53
|
+
|
54
|
+
You can also include `Wisper.publisher` instead of `Wisper::Publisher`.
|
48
55
|
|
49
56
|
### Subscribing
|
50
57
|
|
51
|
-
####
|
58
|
+
#### Objects
|
52
59
|
|
53
|
-
Any object can be a listener
|
60
|
+
Any object can be subscribed as a listener.
|
54
61
|
|
55
62
|
```ruby
|
56
|
-
|
57
|
-
my_publisher.subscribe(MyListener.new)
|
58
|
-
```
|
63
|
+
cancel_order = CancelOrder.new
|
59
64
|
|
60
|
-
|
65
|
+
cancel_order.subscribe(OrderNotifier.new)
|
61
66
|
|
62
|
-
|
67
|
+
cancel_order.call(order_id)
|
68
|
+
```
|
69
|
+
|
70
|
+
The listener would need to implement a method for every event it wishes to receive.
|
63
71
|
|
64
72
|
```ruby
|
65
|
-
|
66
|
-
|
67
|
-
|
73
|
+
class OrderNotifier
|
74
|
+
def cancel_order_successful(order_id)
|
75
|
+
order = Order.find_by_id(order_id)
|
76
|
+
|
77
|
+
# notify someone ...
|
78
|
+
end
|
68
79
|
end
|
69
80
|
```
|
70
81
|
|
71
|
-
|
82
|
+
#### Blocks
|
83
|
+
|
84
|
+
Blocks can be subscribed to single events and can be chained.
|
72
85
|
|
73
86
|
```ruby
|
74
|
-
|
75
|
-
```
|
87
|
+
cancel_order = CancelOrder.new
|
76
88
|
|
77
|
-
|
78
|
-
|
79
|
-
|
89
|
+
cancel_order.on(:cancel_order_successful) { |order_id| ... }
|
90
|
+
.on(:cancel_order_failed) { |order_id| ... }
|
91
|
+
|
92
|
+
cancel_order.call(order_id)
|
93
|
+
```
|
80
94
|
|
81
|
-
###
|
95
|
+
### Handling Events Asynchronously
|
82
96
|
|
83
97
|
```ruby
|
84
|
-
|
85
|
-
|
98
|
+
cancel_order.subscribe(OrderNotifier.new, async: true)
|
99
|
+
```
|
86
100
|
|
87
|
-
|
101
|
+
Wisper has various adapters for asynchronous event handling, please refer to
|
102
|
+
[wisper-celluloid](https://github.com/krisleech/wisper-celluloid),
|
103
|
+
[wisper-sidekiq](https://github.com/krisleech/wisper-sidekiq) or
|
104
|
+
[wisper-activejob](https://github.com/krisleech/wisper-activejob).
|
88
105
|
|
89
|
-
|
90
|
-
assign_attributes(_attrs) if _attrs.present?
|
91
|
-
if valid?
|
92
|
-
save!
|
93
|
-
publish(:create_bid_successful, self)
|
94
|
-
else
|
95
|
-
publish(:create_bid_failed, self)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
```
|
106
|
+
Depending on the adapter used the listener may need to be a class instead of an object.
|
100
107
|
|
101
108
|
### ActionController
|
102
109
|
|
103
110
|
```ruby
|
104
|
-
class
|
105
|
-
|
106
|
-
@bid = Bid.new
|
107
|
-
end
|
108
|
-
|
111
|
+
class CancelOrderController < ApplicationController
|
112
|
+
|
109
113
|
def create
|
110
|
-
|
114
|
+
cancel_order = CancelOrder.new
|
111
115
|
|
112
|
-
|
113
|
-
|
114
|
-
|
116
|
+
cancel_order.subscribe(OrderMailer, async: true)
|
117
|
+
cancel_order.subscribe(ActivityRecorder, async: true)
|
118
|
+
cancel_order.subscribe(StatisticsRecorder, async: true)
|
115
119
|
|
116
|
-
|
117
|
-
|
120
|
+
cancel_order.on(:cancel_order_successful) { |order_id| redirect_to order_path(order_id) }
|
121
|
+
cancel_order.on(:cancel_order_failed) { |order_id| render action: :new }
|
118
122
|
|
119
|
-
|
123
|
+
cancel_order.call(order_id)
|
120
124
|
end
|
121
125
|
end
|
122
126
|
```
|
123
127
|
|
124
|
-
|
125
|
-
|
126
|
-
### Service/Use Case/Command objects
|
128
|
+
### ActiveRecord
|
127
129
|
|
128
|
-
|
129
|
-
than one model, accesses an external API or would burden a model with too much
|
130
|
-
responsibility.
|
130
|
+
If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks:
|
131
131
|
|
132
132
|
```ruby
|
133
|
-
class
|
133
|
+
class Order < ActiveRecord::Base
|
134
134
|
include Wisper::Publisher
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
def initialize(player, team)
|
139
|
-
@player = player
|
140
|
-
@team = team
|
141
|
-
end
|
142
|
-
|
143
|
-
def execute
|
144
|
-
membership = Membership.new(player, team)
|
145
|
-
|
146
|
-
if membership.valid?
|
147
|
-
membership.save!
|
148
|
-
email_player
|
149
|
-
assign_first_mission
|
150
|
-
publish(:player_joining_team_successful, player, team)
|
151
|
-
else
|
152
|
-
publish(:player_joining_team_failed, player, team)
|
153
|
-
end
|
154
|
-
end
|
135
|
+
|
136
|
+
after_commit :publish_creation_successful, on: :create
|
137
|
+
after_validation :publish_creation_failed, on: :create
|
155
138
|
|
156
139
|
private
|
157
140
|
|
158
|
-
def
|
159
|
-
|
141
|
+
def publish_creation_successful
|
142
|
+
broadcast(:order_creation_successful, self)
|
160
143
|
end
|
161
144
|
|
162
|
-
def
|
163
|
-
|
145
|
+
def publish_creation_failed
|
146
|
+
broadcast(:order_creation_failed, self) if errors.any?
|
164
147
|
end
|
165
148
|
end
|
166
149
|
```
|
167
150
|
|
168
|
-
|
169
|
-
|
170
|
-
These are typical app wide listeners which have a method for pretty much every
|
171
|
-
event which is broadcast.
|
172
|
-
|
173
|
-
```ruby
|
174
|
-
class PusherListener
|
175
|
-
def create_thing_successful(thing)
|
176
|
-
# ...
|
177
|
-
end
|
178
|
-
end
|
151
|
+
There are more examples in the [Wiki](https://github.com/krisleech/wisper/wiki).
|
179
152
|
|
180
|
-
|
181
|
-
def create_thing_successful(thing)
|
182
|
-
# ...
|
183
|
-
end
|
184
|
-
end
|
153
|
+
## Global Listeners
|
185
154
|
|
186
|
-
|
187
|
-
def create_thing_successful(thing)
|
188
|
-
# ...
|
189
|
-
end
|
190
|
-
end
|
155
|
+
Global listeners receive all broadcast events which they can respond to.
|
191
156
|
|
192
|
-
|
193
|
-
def create_thing_successful(thing)
|
194
|
-
# ...
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
class IndexingListener
|
199
|
-
def create_thing_successful(thing)
|
200
|
-
# ...
|
201
|
-
end
|
202
|
-
end
|
203
|
-
```
|
204
|
-
|
205
|
-
## Global listeners
|
206
|
-
|
207
|
-
If you become tired of adding the same listeners to _every_ publisher you can
|
208
|
-
add listeners globally. They receive all broadcast events which they can respond
|
209
|
-
to.
|
210
|
-
|
211
|
-
Global listeners should be used with caution, the execution path becomes less
|
212
|
-
obvious on reading the code and of course you are introducing global state and
|
213
|
-
'always on' behaviour. This may not desirable.
|
157
|
+
This is useful for cross cutting concerns such as recording statistics, indexing, caching and logging.
|
214
158
|
|
215
159
|
```ruby
|
216
160
|
Wisper.subscribe(MyListener.new)
|
@@ -220,7 +164,7 @@ In a Rails app you might want to add your global listeners in an initalizer.
|
|
220
164
|
|
221
165
|
Global listeners are threadsafe.
|
222
166
|
|
223
|
-
### Scoping
|
167
|
+
### Scoping by publisher class
|
224
168
|
|
225
169
|
You might want to globally subscribe a listener to publishers with a certain
|
226
170
|
class.
|
@@ -232,7 +176,7 @@ Wisper.subscribe(MyListener.new, scope: :MyPublisher)
|
|
232
176
|
This will subscribe the listener to all instances of `MyPublisher` and its
|
233
177
|
subclasses.
|
234
178
|
|
235
|
-
Alternatively you can also do exactly the same with a publisher class:
|
179
|
+
Alternatively you can also do exactly the same with a publisher class itself:
|
236
180
|
|
237
181
|
```ruby
|
238
182
|
MyPublisher.subscribe(MyListener.new)
|
@@ -249,8 +193,9 @@ end
|
|
249
193
|
```
|
250
194
|
|
251
195
|
Any events broadcast within the block by any publisher will be sent to the
|
252
|
-
listeners.
|
253
|
-
|
196
|
+
listeners.
|
197
|
+
|
198
|
+
This is useful for capturing events published by objects to which you do not have access in a given context.
|
254
199
|
|
255
200
|
Temporary Global Listeners are threadsafe.
|
256
201
|
|
@@ -307,12 +252,8 @@ report_creator.subscribe(MailResponder.new, on: :create_report_failed,
|
|
307
252
|
with: :failed)
|
308
253
|
```
|
309
254
|
|
310
|
-
|
311
|
-
|
312
|
-
```ruby
|
313
|
-
post.on(:success) { |post| redirect_to post }
|
314
|
-
.on(:failure) { |post| render action: :edit, locals: { post: post } }
|
315
|
-
```
|
255
|
+
You could also alias the method within your listener, as such
|
256
|
+
`alias successful create_report_successful`.
|
316
257
|
|
317
258
|
## RSpec
|
318
259
|
|
@@ -346,18 +287,19 @@ publisher.execute
|
|
346
287
|
|
347
288
|
### Stubbing publishers
|
348
289
|
|
349
|
-
|
350
|
-
isolation tests that only care about reacting to events.
|
290
|
+
You can stub publishers and their events in unit (isolated) tests that only care about reacting to events.
|
351
291
|
|
352
292
|
Given this piece of code:
|
353
293
|
|
354
294
|
```ruby
|
355
|
-
class
|
356
|
-
def
|
295
|
+
class MyController
|
296
|
+
def create
|
357
297
|
publisher = MyPublisher.new
|
298
|
+
|
358
299
|
publisher.on(:some_event) do |variable|
|
359
300
|
return "Hello with #{variable}!"
|
360
301
|
end
|
302
|
+
|
361
303
|
publisher.execute
|
362
304
|
end
|
363
305
|
end
|
@@ -368,24 +310,21 @@ You can test it like this:
|
|
368
310
|
```ruby
|
369
311
|
require 'wisper/rspec/stub_wisper_publisher'
|
370
312
|
|
371
|
-
describe
|
313
|
+
describe MyController do
|
372
314
|
context "on some_event" do
|
373
315
|
before do
|
374
316
|
stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo")
|
375
317
|
end
|
376
318
|
|
377
319
|
it "renders" do
|
378
|
-
response =
|
320
|
+
response = MyController.new.create
|
379
321
|
expect(response).to eq "Hello with foo!"
|
380
322
|
end
|
381
323
|
end
|
382
324
|
end
|
383
325
|
```
|
384
326
|
|
385
|
-
This
|
386
|
-
isolation from the business logic. This technique is used at the controller
|
387
|
-
layer to isolate testing the controller from testing the encapsulated business
|
388
|
-
logic.
|
327
|
+
This is useful when testing Rails controllers in isolation from the business logic.
|
389
328
|
|
390
329
|
You can use any number of args to pass to the event:
|
391
330
|
|
@@ -395,6 +334,15 @@ stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo1", "foo2", ...)
|
|
395
334
|
|
396
335
|
See `spec/lib/rspec_extensions_spec.rb` for a runnable example.
|
397
336
|
|
337
|
+
## Clearing Global Listeners
|
338
|
+
|
339
|
+
If you use global listeners in non-feature tests you _might_ want to clear them
|
340
|
+
in a hook to prevent global subscriptions persisting between tests.
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
after { Wisper.clear }
|
344
|
+
```
|
345
|
+
|
398
346
|
## Compatibility
|
399
347
|
|
400
348
|
Tested with MRI 1.9.x, MRI 2.0.0, JRuby (1.9 and 2.0 mode) and Rubinius (1.9
|
data/lib/wisper.rb
CHANGED
@@ -26,17 +26,36 @@ module Wisper
|
|
26
26
|
self.subscribe(listener, options)
|
27
27
|
end
|
28
28
|
|
29
|
+
# Examples:
|
30
|
+
#
|
31
|
+
# Wisper.subscribe(AuditRecorder.new)
|
32
|
+
#
|
33
|
+
# Wisper.subscribe(AuditRecorder.new, StatsRecorder.new)
|
34
|
+
#
|
35
|
+
# Wisper.subscribe(AuditRecorder.new, on: 'order_created')
|
36
|
+
#
|
37
|
+
# Wisper.subscribe(AuditRecorder.new, scope: 'MyPublisher')
|
38
|
+
#
|
39
|
+
# Wisper.subscribe(AuditRecorder.new, StatsRecorder.new) do
|
40
|
+
# # ..
|
41
|
+
# end
|
42
|
+
#
|
29
43
|
def self.subscribe(*args, &block)
|
30
44
|
if block_given?
|
31
|
-
TemporaryListeners.
|
45
|
+
TemporaryListeners.subscribe(*args, &block)
|
32
46
|
else
|
33
|
-
|
34
|
-
args.each do |listener|
|
35
|
-
GlobalListeners.add(listener, options)
|
36
|
-
end
|
47
|
+
GlobalListeners.subscribe(*args)
|
37
48
|
end
|
38
49
|
end
|
39
50
|
|
51
|
+
def self.publisher
|
52
|
+
Publisher
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.clear
|
56
|
+
GlobalListeners.clear
|
57
|
+
end
|
58
|
+
|
40
59
|
def self.configure
|
41
60
|
yield(configuration)
|
42
61
|
end
|
data/lib/wisper/configuration.rb
CHANGED
@@ -1,18 +1,24 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
|
+
# Handles global subscriptions
|
4
|
+
|
3
5
|
module Wisper
|
4
6
|
class GlobalListeners
|
5
7
|
include Singleton
|
6
|
-
attr_reader :mutex
|
7
|
-
private :mutex
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
@registrations = Set.new
|
11
11
|
@mutex = Mutex.new
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
14
|
+
def subscribe(*listeners)
|
15
|
+
options = listeners.last.is_a?(Hash) ? listeners.pop : {}
|
16
|
+
|
17
|
+
with_mutex do
|
18
|
+
listeners.each do |listener|
|
19
|
+
@registrations << ObjectRegistration.new(listener, options)
|
20
|
+
end
|
21
|
+
end
|
16
22
|
self
|
17
23
|
end
|
18
24
|
|
@@ -28,8 +34,8 @@ module Wisper
|
|
28
34
|
with_mutex { @registrations.clear }
|
29
35
|
end
|
30
36
|
|
31
|
-
def self.
|
32
|
-
instance.
|
37
|
+
def self.subscribe(*listeners)
|
38
|
+
instance.subscribe(*listeners)
|
33
39
|
end
|
34
40
|
|
35
41
|
def self.registrations
|
@@ -44,16 +50,15 @@ module Wisper
|
|
44
50
|
instance.clear
|
45
51
|
end
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
add(listener, options)
|
53
|
+
def self.add_listener(listener, options = {}) # deprecated
|
54
|
+
warn "[DEPRECATION] use `subscribe` instead of `add_listener`"
|
55
|
+
subscribe(listener, options)
|
51
56
|
end
|
52
57
|
|
53
58
|
private
|
54
59
|
|
55
60
|
def with_mutex
|
56
|
-
mutex.synchronize { yield }
|
61
|
+
@mutex.synchronize { yield }
|
57
62
|
end
|
58
63
|
end
|
59
64
|
end
|
data/lib/wisper/publisher.rb
CHANGED
@@ -4,31 +4,42 @@ module Wisper
|
|
4
4
|
registrations.map(&:listener).freeze
|
5
5
|
end
|
6
6
|
|
7
|
-
def
|
7
|
+
def subscribe(listener, options = {})
|
8
8
|
local_registrations << ObjectRegistration.new(listener, options)
|
9
9
|
self
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
def on(*events, &block)
|
13
|
+
raise ArgumentError, 'must give at least one event' if events.empty?
|
14
|
+
local_registrations << BlockRegistration.new(block, on: events)
|
15
|
+
self
|
16
|
+
end
|
13
17
|
|
14
18
|
def add_block_listener(options = {}, &block)
|
19
|
+
warn "[DEPRECATED] use `on` instead of `add_block_listener`"
|
15
20
|
local_registrations << BlockRegistration.new(block, options)
|
16
21
|
self
|
17
22
|
end
|
18
23
|
|
19
|
-
|
20
|
-
|
21
|
-
|
24
|
+
def add_listener(listener, options = {})
|
25
|
+
warn "[DEPRECATED] use `subscribe` instead of `add_listener`"
|
26
|
+
subscribe(listener, options)
|
22
27
|
end
|
23
28
|
|
24
|
-
|
29
|
+
def respond_to(*events, &block)
|
30
|
+
warn '[DEPRECATED] use `on` instead of `respond_to`'
|
31
|
+
on(*events, &block)
|
32
|
+
end
|
25
33
|
|
26
34
|
module ClassMethods
|
27
|
-
def
|
28
|
-
GlobalListeners.
|
35
|
+
def subscribe(listener, options = {})
|
36
|
+
GlobalListeners.subscribe(listener, options.merge(:scope => self))
|
29
37
|
end
|
30
38
|
|
31
|
-
|
39
|
+
def add_listener(listener, options = {})
|
40
|
+
warn "[DEPRECATED] use `subscribe` instead of `add_listener`"
|
41
|
+
subscribe(listener, options)
|
42
|
+
end
|
32
43
|
end
|
33
44
|
|
34
45
|
private
|
@@ -1,22 +1,25 @@
|
|
1
|
+
# Handles temporary global subscriptions
|
2
|
+
|
1
3
|
module Wisper
|
2
4
|
class TemporaryListeners
|
3
5
|
|
4
|
-
def self.
|
5
|
-
|
6
|
-
new.with(listeners, options, &block)
|
6
|
+
def self.subscribe(*listeners, &block)
|
7
|
+
new.subscribe(*listeners, &block)
|
7
8
|
end
|
8
9
|
|
9
10
|
def self.registrations
|
10
11
|
new.registrations
|
11
12
|
end
|
12
13
|
|
13
|
-
def
|
14
|
+
def subscribe(*listeners, &block)
|
15
|
+
options = listeners.last.is_a?(Hash) ? listeners.pop : {}
|
14
16
|
begin
|
15
|
-
|
17
|
+
listeners.each { |listener| registrations << ObjectRegistration.new(listener, options) }
|
16
18
|
yield
|
17
19
|
ensure
|
18
20
|
clear
|
19
21
|
end
|
22
|
+
self
|
20
23
|
end
|
21
24
|
|
22
25
|
def registrations
|
@@ -29,14 +32,6 @@ module Wisper
|
|
29
32
|
registrations.clear
|
30
33
|
end
|
31
34
|
|
32
|
-
def add_listeners(listeners, options)
|
33
|
-
listeners.each { |listener| add_listener(listener, options)}
|
34
|
-
end
|
35
|
-
|
36
|
-
def add_listener(listener, options)
|
37
|
-
registrations << ObjectRegistration.new(listener, options)
|
38
|
-
end
|
39
|
-
|
40
35
|
def key
|
41
36
|
'__wisper_temporary_listeners'
|
42
37
|
end
|
data/lib/wisper/version.rb
CHANGED
@@ -3,15 +3,15 @@ describe Wisper::GlobalListeners do
|
|
3
3
|
let(:local_listener) { double('listener') }
|
4
4
|
let(:publisher) { publisher_class.new }
|
5
5
|
|
6
|
-
describe '.
|
6
|
+
describe '.subscribe' do
|
7
7
|
it 'adds given listener to every publisher' do
|
8
|
-
Wisper::GlobalListeners.
|
8
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
9
9
|
expect(global_listener).to receive(:it_happened)
|
10
10
|
publisher.send(:broadcast, :it_happened)
|
11
11
|
end
|
12
12
|
|
13
13
|
it 'works with options' do
|
14
|
-
Wisper::GlobalListeners.
|
14
|
+
Wisper::GlobalListeners.subscribe(global_listener, :on => :it_happened,
|
15
15
|
:with => :woot)
|
16
16
|
expect(global_listener).to receive(:woot).once
|
17
17
|
expect(global_listener).not_to receive(:it_happened_again)
|
@@ -21,10 +21,10 @@ describe Wisper::GlobalListeners do
|
|
21
21
|
|
22
22
|
it 'works along side local listeners' do
|
23
23
|
# global listener
|
24
|
-
Wisper::GlobalListeners.
|
24
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
25
25
|
|
26
26
|
# local listener
|
27
|
-
publisher.
|
27
|
+
publisher.subscribe(local_listener)
|
28
28
|
|
29
29
|
expect(global_listener).to receive(:it_happened)
|
30
30
|
expect(local_listener).to receive(:it_happened)
|
@@ -37,7 +37,7 @@ describe Wisper::GlobalListeners do
|
|
37
37
|
publisher_2 = publisher_class.new
|
38
38
|
publisher_3 = publisher_class.new
|
39
39
|
|
40
|
-
Wisper::GlobalListeners.
|
40
|
+
Wisper::GlobalListeners.subscribe(global_listener, :scope => [publisher_1.class,
|
41
41
|
publisher_2.class])
|
42
42
|
|
43
43
|
expect(global_listener).to receive(:it_happened_1).once
|
@@ -53,7 +53,7 @@ describe Wisper::GlobalListeners do
|
|
53
53
|
num_threads = 100
|
54
54
|
(1..num_threads).to_a.map do
|
55
55
|
Thread.new do
|
56
|
-
Wisper::GlobalListeners.
|
56
|
+
Wisper::GlobalListeners.subscribe(Object.new)
|
57
57
|
sleep(rand) # a little chaos
|
58
58
|
end
|
59
59
|
end.each(&:join)
|
@@ -64,7 +64,7 @@ describe Wisper::GlobalListeners do
|
|
64
64
|
|
65
65
|
describe '.listeners' do
|
66
66
|
it 'returns collection of global listeners' do
|
67
|
-
Wisper::GlobalListeners.
|
67
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
68
68
|
expect(Wisper::GlobalListeners.listeners).to eq [global_listener]
|
69
69
|
end
|
70
70
|
|
@@ -75,17 +75,16 @@ describe Wisper::GlobalListeners do
|
|
75
75
|
end
|
76
76
|
|
77
77
|
it '.clear clears all global listeners' do
|
78
|
-
Wisper::GlobalListeners.
|
78
|
+
Wisper::GlobalListeners.subscribe(global_listener)
|
79
79
|
Wisper::GlobalListeners.clear
|
80
80
|
expect(Wisper::GlobalListeners.listeners).to be_empty
|
81
81
|
end
|
82
82
|
|
83
83
|
describe 'backwards compatibility' do
|
84
|
-
it '.add_listener
|
84
|
+
it '.add_listener is aliased to .add' do
|
85
85
|
silence_warnings do
|
86
|
+
expect(Wisper::GlobalListeners).to receive(:subscribe)
|
86
87
|
Wisper::GlobalListeners.add_listener(global_listener)
|
87
|
-
expect(global_listener).to receive(:it_happened)
|
88
|
-
publisher.send(:broadcast, :it_happened)
|
89
88
|
end
|
90
89
|
end
|
91
90
|
end
|
@@ -19,20 +19,7 @@ describe Wisper do
|
|
19
19
|
|
20
20
|
command = MyCommand.new
|
21
21
|
|
22
|
-
command.
|
23
|
-
|
24
|
-
command.execute(true)
|
25
|
-
end
|
26
|
-
|
27
|
-
it 'subscribes block to all published events' do
|
28
|
-
insider = double('Insider')
|
29
|
-
expect(insider).to receive(:render).with('hello')
|
30
|
-
|
31
|
-
command = MyCommand.new
|
32
|
-
|
33
|
-
command.add_block_listener do |message|
|
34
|
-
insider.render(message)
|
35
|
-
end
|
22
|
+
command.subscribe(listener)
|
36
23
|
|
37
24
|
command.execute(true)
|
38
25
|
end
|
@@ -45,8 +32,8 @@ describe Wisper do
|
|
45
32
|
|
46
33
|
command = MyCommand.new
|
47
34
|
|
48
|
-
command.
|
49
|
-
command.
|
35
|
+
command.subscribe(listener_1, :on => :success, :with => :happy_days)
|
36
|
+
command.subscribe(listener_2, :on => :failure, :with => :sad_days)
|
50
37
|
|
51
38
|
command.execute(true)
|
52
39
|
command.execute(false)
|
@@ -3,13 +3,13 @@ describe Wisper::TemporaryListeners do
|
|
3
3
|
let(:listener_2) { double('listener', :to_a => nil) }
|
4
4
|
let(:publisher) { Object.class_eval { include Wisper::Publisher } }
|
5
5
|
|
6
|
-
describe '.
|
6
|
+
describe '.subscribe' do
|
7
7
|
it 'globally subscribes listener for duration of given block' do
|
8
8
|
|
9
9
|
expect(listener_1).to receive(:success)
|
10
10
|
expect(listener_1).to_not receive(:failure)
|
11
11
|
|
12
|
-
Wisper::TemporaryListeners.
|
12
|
+
Wisper::TemporaryListeners.subscribe(listener_1) do
|
13
13
|
publisher.instance_eval { broadcast(:success) }
|
14
14
|
end
|
15
15
|
|
@@ -24,7 +24,7 @@ describe Wisper::TemporaryListeners do
|
|
24
24
|
expect(listener_2).to receive(:success)
|
25
25
|
expect(listener_2).to_not receive(:failure)
|
26
26
|
|
27
|
-
Wisper::TemporaryListeners.
|
27
|
+
Wisper::TemporaryListeners.subscribe(listener_1, listener_2) do
|
28
28
|
publisher.instance_eval { broadcast(:success) }
|
29
29
|
end
|
30
30
|
|
@@ -45,7 +45,7 @@ describe Wisper::TemporaryListeners do
|
|
45
45
|
|
46
46
|
it 'ensures registrations are cleared after exception raised in block' do
|
47
47
|
begin
|
48
|
-
Wisper::TemporaryListeners.
|
48
|
+
Wisper::TemporaryListeners.subscribe(listener_1) do
|
49
49
|
raise StandardError
|
50
50
|
end
|
51
51
|
rescue StandardError
|
@@ -54,6 +54,10 @@ describe Wisper::TemporaryListeners do
|
|
54
54
|
expect(Wisper::TemporaryListeners.registrations.size).to eql 0
|
55
55
|
end
|
56
56
|
end
|
57
|
+
|
58
|
+
it 'returns self so methods can be chained' do
|
59
|
+
expect(Wisper::TemporaryListeners.subscribe {}).to be_an_instance_of(Wisper::TemporaryListeners)
|
60
|
+
end
|
57
61
|
end
|
58
62
|
|
59
63
|
# [1] stubbing `to_a` prevents `Double "listener" received unexpected message
|
@@ -2,12 +2,12 @@ describe Wisper::Publisher do
|
|
2
2
|
let(:listener) { double('listener') }
|
3
3
|
let(:publisher) { publisher_class.new }
|
4
4
|
|
5
|
-
describe '.
|
5
|
+
describe '.subscribe' do
|
6
6
|
it 'subscribes given listener to all published events' do
|
7
7
|
expect(listener).to receive(:this_happened)
|
8
8
|
expect(listener).to receive(:so_did_this)
|
9
9
|
|
10
|
-
publisher.
|
10
|
+
publisher.subscribe(listener)
|
11
11
|
|
12
12
|
publisher.send(:broadcast, 'this_happened')
|
13
13
|
publisher.send(:broadcast, 'so_did_this')
|
@@ -21,7 +21,7 @@ describe Wisper::Publisher do
|
|
21
21
|
|
22
22
|
expect(listener).to respond_to(:so_did_this)
|
23
23
|
|
24
|
-
publisher.
|
24
|
+
publisher.subscribe(listener, :on => 'this_happened')
|
25
25
|
|
26
26
|
publisher.send(:broadcast, 'this_happened')
|
27
27
|
publisher.send(:broadcast, 'so_did_this')
|
@@ -35,7 +35,7 @@ describe Wisper::Publisher do
|
|
35
35
|
|
36
36
|
expect(listener).to respond_to(:so_did_this)
|
37
37
|
|
38
|
-
publisher.
|
38
|
+
publisher.subscribe(listener, :on => ['this_happened', 'and_this'])
|
39
39
|
|
40
40
|
publisher.send(:broadcast, 'this_happened')
|
41
41
|
publisher.send(:broadcast, 'so_did_this')
|
@@ -47,7 +47,7 @@ describe Wisper::Publisher do
|
|
47
47
|
it 'sets method to call listener with on event' do
|
48
48
|
expect(listener).to receive(:different_method).twice
|
49
49
|
|
50
|
-
publisher.
|
50
|
+
publisher.subscribe(listener, :with => :different_method)
|
51
51
|
|
52
52
|
publisher.send(:broadcast, 'this_happened')
|
53
53
|
publisher.send(:broadcast, 'so_did_this')
|
@@ -59,7 +59,7 @@ describe Wisper::Publisher do
|
|
59
59
|
expect(listener).to receive(:after_it_happened)
|
60
60
|
expect(listener).not_to receive(:it_happened)
|
61
61
|
|
62
|
-
publisher.
|
62
|
+
publisher.subscribe(listener, :prefix => :after)
|
63
63
|
|
64
64
|
publisher.send(:broadcast, 'it_happened')
|
65
65
|
end
|
@@ -68,7 +68,7 @@ describe Wisper::Publisher do
|
|
68
68
|
expect(listener).to receive(:on_it_happened)
|
69
69
|
expect(listener).not_to receive(:it_happened)
|
70
70
|
|
71
|
-
publisher.
|
71
|
+
publisher.subscribe(listener, :prefix => true)
|
72
72
|
|
73
73
|
publisher.send(:broadcast, 'it_happened')
|
74
74
|
end
|
@@ -83,16 +83,16 @@ describe Wisper::Publisher do
|
|
83
83
|
it 'scopes listener to given class' do
|
84
84
|
expect(listener_1).to receive(:it_happended)
|
85
85
|
expect(listener_2).not_to receive(:it_happended)
|
86
|
-
publisher.
|
87
|
-
publisher.
|
86
|
+
publisher.subscribe(listener_1, :scope => publisher.class)
|
87
|
+
publisher.subscribe(listener_2, :scope => Class.new)
|
88
88
|
publisher.send(:broadcast, 'it_happended')
|
89
89
|
end
|
90
90
|
|
91
91
|
it 'scopes listener to given class string' do
|
92
92
|
expect(listener_1).to receive(:it_happended)
|
93
93
|
expect(listener_2).not_to receive(:it_happended)
|
94
|
-
publisher.
|
95
|
-
publisher.
|
94
|
+
publisher.subscribe(listener_1, :scope => publisher.class.to_s)
|
95
|
+
publisher.subscribe(listener_2, :scope => Class.new.to_s)
|
96
96
|
publisher.send(:broadcast, 'it_happended')
|
97
97
|
end
|
98
98
|
|
@@ -105,7 +105,7 @@ describe Wisper::Publisher do
|
|
105
105
|
|
106
106
|
publisher = publisher_sub_klass.new
|
107
107
|
|
108
|
-
publisher.
|
108
|
+
publisher.subscribe(listener, :scope => publisher_super_klass)
|
109
109
|
publisher.send(:broadcast, 'it_happended')
|
110
110
|
end
|
111
111
|
end
|
@@ -171,7 +171,7 @@ describe Wisper::Publisher do
|
|
171
171
|
end
|
172
172
|
|
173
173
|
it 'returns publisher so methods can be chained' do
|
174
|
-
expect(publisher.
|
174
|
+
expect(publisher.subscribe(listener, :on => 'so_did_this')).to \
|
175
175
|
eq publisher
|
176
176
|
end
|
177
177
|
|
@@ -180,14 +180,61 @@ describe Wisper::Publisher do
|
|
180
180
|
end
|
181
181
|
end
|
182
182
|
|
183
|
+
describe '.on' do
|
184
|
+
let(:insider) { double('insider') }
|
185
|
+
|
186
|
+
it 'subscribes block to given event' do
|
187
|
+
expect(insider).to receive(:yes).once
|
188
|
+
|
189
|
+
publisher.on(:something_happened) do
|
190
|
+
insider.yes
|
191
|
+
end
|
192
|
+
|
193
|
+
publisher.send(:broadcast, :something_happened)
|
194
|
+
publisher.send(:broadcast, :and_so_did_this)
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'subscribes block to given events' do
|
198
|
+
expect(insider).to receive(:yes).twice
|
199
|
+
|
200
|
+
publisher.on(:something_happened, :and_so_did_this) do
|
201
|
+
insider.yes
|
202
|
+
end
|
203
|
+
|
204
|
+
publisher.send(:broadcast, :something_happened)
|
205
|
+
publisher.send(:broadcast, :and_so_did_this)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'raise an error if no events given' do
|
209
|
+
expect { publisher.on() {} }.to raise_error(ArgumentError)
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'returns publisher so methods can be chained' do
|
213
|
+
expect(publisher.on(:foo) {}).to eq publisher
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @deprecated
|
218
|
+
describe '.add_listener' do
|
219
|
+
it 'is aliased to .subscribe' do
|
220
|
+
expect(publisher).to receive(:subscribe)
|
221
|
+
silence_warnings do
|
222
|
+
publisher.add_listener(listener)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# @deprecated
|
183
228
|
describe '.add_block_listener' do
|
184
229
|
let(:insider) { double('insider') }
|
185
230
|
|
186
231
|
it 'subscribes given block to all events' do
|
187
232
|
expect(insider).to receive(:it_happened).twice
|
188
233
|
|
189
|
-
|
190
|
-
|
234
|
+
silence_warnings do
|
235
|
+
publisher.add_block_listener do
|
236
|
+
insider.it_happened
|
237
|
+
end
|
191
238
|
end
|
192
239
|
|
193
240
|
publisher.send(:broadcast, 'something_happened')
|
@@ -198,8 +245,10 @@ describe Wisper::Publisher do
|
|
198
245
|
it '.add_block_listener subscribes block to an event' do
|
199
246
|
expect(insider).not_to receive(:it_happened).once
|
200
247
|
|
201
|
-
|
202
|
-
|
248
|
+
silence_warnings do
|
249
|
+
publisher.add_block_listener(:on => 'something_happened') do
|
250
|
+
insider.it_happened
|
251
|
+
end
|
203
252
|
end
|
204
253
|
|
205
254
|
publisher.send(:broadcast, 'something_happened')
|
@@ -209,9 +258,11 @@ describe Wisper::Publisher do
|
|
209
258
|
it '.add_block_listener subscribes block to all listed events' do
|
210
259
|
expect(insider).to receive(:it_happened).twice
|
211
260
|
|
212
|
-
|
213
|
-
|
214
|
-
|
261
|
+
silence_warnings do
|
262
|
+
publisher.add_block_listener(
|
263
|
+
:on => ['something_happened', 'and_so_did_this']) do
|
264
|
+
insider.it_happened
|
265
|
+
end
|
215
266
|
end
|
216
267
|
|
217
268
|
publisher.send(:broadcast, 'something_happened')
|
@@ -221,34 +272,20 @@ describe Wisper::Publisher do
|
|
221
272
|
end
|
222
273
|
|
223
274
|
it 'returns publisher so methods can be chained' do
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
end
|
228
|
-
|
229
|
-
describe '.on (alternative block syntax)' do
|
230
|
-
let(:insider) { double('insider') }
|
231
|
-
|
232
|
-
it 'subscribes given block to an event' do
|
233
|
-
expect(insider).to receive(:it_happened)
|
234
|
-
|
235
|
-
publisher.on(:something_happened) do
|
236
|
-
insider.it_happened
|
275
|
+
silence_warnings do
|
276
|
+
expect(publisher.add_block_listener(:on => 'this_thing_happened') do
|
277
|
+
end).to eq publisher
|
237
278
|
end
|
238
|
-
|
239
|
-
publisher.send(:broadcast, 'something_happened')
|
240
279
|
end
|
280
|
+
end
|
241
281
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
publisher.on(:
|
246
|
-
|
282
|
+
# @deprecated
|
283
|
+
describe '.respond_to (alternative block syntax)' do
|
284
|
+
it 'delegates to .on' do
|
285
|
+
expect(publisher).to receive(:on).with(:foobar)
|
286
|
+
silence_warnings do
|
287
|
+
publisher.respond_to(:foobar) { }
|
247
288
|
end
|
248
|
-
|
249
|
-
publisher.send(:broadcast, 'something_happened')
|
250
|
-
publisher.send(:broadcast, 'and_so_did_this')
|
251
|
-
publisher.send(:broadcast, 'but_not_this')
|
252
289
|
end
|
253
290
|
end
|
254
291
|
|
@@ -257,7 +294,7 @@ describe Wisper::Publisher do
|
|
257
294
|
expect(listener).not_to receive(:so_did_this)
|
258
295
|
allow(listener).to receive(:respond_to?).and_return(false)
|
259
296
|
|
260
|
-
publisher.
|
297
|
+
publisher.subscribe(listener, :on => 'so_did_this')
|
261
298
|
|
262
299
|
publisher.send(:broadcast, 'so_did_this')
|
263
300
|
end
|
@@ -266,7 +303,7 @@ describe Wisper::Publisher do
|
|
266
303
|
it 'is indifferent to string and symbol' do
|
267
304
|
expect(listener).to receive(:this_happened).twice
|
268
305
|
|
269
|
-
publisher.
|
306
|
+
publisher.subscribe(listener)
|
270
307
|
|
271
308
|
publisher.send(:broadcast, 'this_happened')
|
272
309
|
publisher.send(:broadcast, :this_happened)
|
@@ -275,7 +312,7 @@ describe Wisper::Publisher do
|
|
275
312
|
it 'is indifferent to dasherized and underscored strings' do
|
276
313
|
expect(listener).to receive(:this_happened).twice
|
277
314
|
|
278
|
-
publisher.
|
315
|
+
publisher.subscribe(listener)
|
279
316
|
|
280
317
|
publisher.send(:broadcast, 'this_happened')
|
281
318
|
publisher.send(:broadcast, 'this-happened')
|
@@ -290,25 +327,30 @@ describe Wisper::Publisher do
|
|
290
327
|
end
|
291
328
|
|
292
329
|
it 'returns local listeners' do
|
293
|
-
publisher.
|
330
|
+
publisher.subscribe(listener)
|
294
331
|
expect(publisher.listeners).to eq [listener]
|
295
332
|
expect(publisher.listeners.size).to eq 1
|
296
333
|
end
|
297
334
|
end
|
298
335
|
|
299
|
-
describe '#
|
336
|
+
describe '#subscribe' do
|
300
337
|
let(:publisher_klass_1) { publisher_class }
|
301
338
|
let(:publisher_klass_2) { publisher_class }
|
302
339
|
|
303
|
-
it 'subscribes
|
304
|
-
publisher_klass_1.
|
340
|
+
it 'subscribes listener to all instances of publisher' do
|
341
|
+
publisher_klass_1.subscribe(listener)
|
305
342
|
expect(listener).to receive(:it_happened).once
|
306
343
|
publisher_klass_1.new.send(:broadcast, 'it_happened')
|
307
344
|
publisher_klass_2.new.send(:broadcast, 'it_happened')
|
308
345
|
end
|
346
|
+
end
|
309
347
|
|
348
|
+
describe '#add_listener' do # deprecated
|
310
349
|
it 'is aliased to #subscribe' do
|
311
|
-
expect(
|
350
|
+
expect(publisher).to receive(:subscribe)
|
351
|
+
silence_warnings do
|
352
|
+
publisher.add_listener(listener)
|
353
|
+
end
|
312
354
|
end
|
313
355
|
end
|
314
356
|
end
|
data/spec/lib/wisper_spec.rb
CHANGED
@@ -78,6 +78,16 @@ describe Wisper do
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
it '.publisher returns the Publisher module' do
|
82
|
+
expect(Wisper.publisher).to eq Wisper::Publisher
|
83
|
+
end
|
84
|
+
|
85
|
+
it '.clear clears all global listeners' do
|
86
|
+
10.times { Wisper.subscribe(double) }
|
87
|
+
Wisper.clear
|
88
|
+
expect(Wisper::GlobalListeners.listeners).to be_empty
|
89
|
+
end
|
90
|
+
|
81
91
|
it '.configuration returns configuration' do
|
82
92
|
expect(Wisper.configuration).to be_an_instance_of(Wisper::Configuration)
|
83
93
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wisper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kris Leech
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: pub/sub for Ruby objects
|
14
14
|
email:
|