wisper 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,10 +3,10 @@
3
3
  Simple pub/sub for Ruby objects
4
4
 
5
5
  [![Code Climate](https://codeclimate.com/github/krisleech/wisper.png)](https://codeclimate.com/github/krisleech/wisper)
6
- [![Build Status](https://travis-ci.org/krisleech/wisper.png)](https://travis-ci.org/krisleech/wisper)
6
+ [![Build Status](https://travis-ci.org/krisleech/wisper.png?branch=master)](https://travis-ci.org/krisleech/wisper)
7
7
 
8
8
  While this is not dependent on Rails in any way it was extracted from a Rails
9
- project and is used as an alternative to ActiveRecord callbacks and Observers.
9
+ project and can used as an alternative to ActiveRecord callbacks and Observers.
10
10
 
11
11
  The problem with callbacks and Observers is that they always happen. How many
12
12
  times have you wanted to do `User.create` without firing off a welcome email?
@@ -19,7 +19,9 @@ models.
19
19
 
20
20
  Add this line to your application's Gemfile:
21
21
 
22
- gem 'wisper'
22
+ ```ruby
23
+ gem 'wisper', '~>1.0.0'
24
+ ```
23
25
 
24
26
  ## Usage
25
27
 
@@ -33,6 +35,7 @@ class MyPublisher
33
35
  include Wisper
34
36
 
35
37
  def do_something
38
+ # ...
36
39
  publish(:done_something, self)
37
40
  end
38
41
  end
@@ -49,17 +52,17 @@ publish(:done_something, self, 'hello', 'world')
49
52
 
50
53
  #### Listeners
51
54
 
52
- The listener is subscribed to all events it responds to.
55
+ Any object can be a listener and by default they are only subscribed to events
56
+ they can respond to.
53
57
 
54
58
  ```ruby
55
- listener = Object.new # any object
56
59
  my_publisher = MyPublisher.new
57
- my_publisher.subscribe(listener)
60
+ my_publisher.subscribe(MyListener.new)
58
61
  ```
59
62
 
60
63
  #### Blocks
61
64
 
62
- The block is subscribed to a single event.
65
+ Blocks are subscribed to single events.
63
66
 
64
67
  ```ruby
65
68
  my_publisher = MyPublisher.new
@@ -68,6 +71,23 @@ my_publisher.on(:done_something) do |publisher|
68
71
  end
69
72
  ```
70
73
 
74
+ ### Asynchronous Publishing (Experimental)
75
+
76
+ There is support for publishing events asynchronously by passing the `async`
77
+ option.
78
+
79
+ ```ruby
80
+ my_publisher.add_subscriber(MySubscriber.new, :async => true)
81
+ ```
82
+
83
+ This leans on Celluloid, which must be included in your Gemfile.
84
+
85
+ The listener is transparently turned in to a Celluloid Actor.
86
+
87
+ Please refer to [Celluloid](https://github.com/celluloid/celluloid/wiki)
88
+ for more information, particually the
89
+ [Gotchas](https://github.com/celluloid/celluloid/wiki/Gotchas).
90
+
71
91
  ### ActiveRecord
72
92
 
73
93
  ```ruby
@@ -103,34 +123,38 @@ class PostsController < ApplicationController
103
123
  end
104
124
  ```
105
125
 
106
- ### Service/Use case object
107
-
108
- The downside to publishing directly from ActiveRecord models is that an event
109
- can get fired and then rolled back if a transaction fails.
126
+ ### Service/Use Case/Command objects
110
127
 
111
- Since I am trying to make my models dumb I tend to use a separate service
112
- object which contains all the logic and wraps it all in a transaction.
113
-
114
- The follow is contrived, but you can imagine it doing more than just updating a
115
- record, maybe sending an email or updating other records.
128
+ A Service object is useful when an operation is complex, interacts with more
129
+ than one model, accesses an external API or would burden a model with too much
130
+ responsibility.
116
131
 
117
132
  ```ruby
118
- class CreateThing
133
+ class PlayerJoiningTeam
119
134
  include Wisper
120
135
 
121
- def execute(attributes)
122
- thing = Thing.new(attributes)
136
+ def execute(player, team)
137
+ membership = Membership.new(player, team)
123
138
 
124
- if thing.valid?
125
- ActiveRecord::Base.transaction do
126
- thing.save
127
- # ...
128
- end
129
- publish(:create_thing_successful, thing)
139
+ if membership.valid?
140
+ membership.save!
141
+ email_player(player, team)
142
+ assign_first_mission(player, team)
143
+ publish(:player_joining_team_successful, player, team)
130
144
  else
131
- publish(:create_thing_failed, thing)
145
+ publish(:player_joining_team_failed, player, team)
132
146
  end
133
147
  end
148
+
149
+ private
150
+
151
+ def email_player(player, team)
152
+ # ...
153
+ end
154
+
155
+ def assign_first_mission(player, team)
156
+ # ...
157
+ end
134
158
  end
135
159
  ```
136
160
 
@@ -157,13 +181,40 @@ class StatisticsListener
157
181
  # ...
158
182
  end
159
183
  end
184
+
185
+ class CacheListener
186
+ def create_thing_successful(thing)
187
+ # ...
188
+ end
189
+ end
190
+
191
+ class IndexingListener
192
+ def create_thing_successful(thing)
193
+ # ...
194
+ end
195
+ end
160
196
  ```
161
197
 
198
+ ## Global listeners
199
+
200
+ If you become tired of adding the same listeners to _every_ publisher you can
201
+ add global listeners. They receive all published events which they can respond
202
+ to.
203
+
204
+ However it means that when looking at the code it will not be obvious that the
205
+ global listeners are being executed in additional to the regular listeners.
206
+
207
+ ```ruby
208
+ Wisper::GlobalListeners.add_listener(MyListener.new)
209
+ ```
210
+
211
+ In a Rails app you might want to add your global listeners in an initalizer.
212
+
162
213
  ## Subscribing to selected events
163
214
 
164
- By default a listener will get notified of all events it responds to. You can
165
- limit which events a listener is notified of by passing an event or array of
166
- events to `:on`.
215
+ By default a listener will get notified of all events it can respond to. You
216
+ can limit which events a listener is notified of by passing an event or array
217
+ of events to `:on`.
167
218
 
168
219
  ```ruby
169
220
  post_creater.subscribe(PusherListener.new, :on => :create_post_successful)
@@ -207,6 +258,57 @@ post.on(:success) { |post| redirect_to post }
207
258
  .on(:failure) { |post| render :action => :edit, :locals => :post => post }
208
259
  ```
209
260
 
261
+ ## RSpec
262
+
263
+ Wisper comes with a method for stubbing event publishers so that you can create isolation tests
264
+ that only care about reacting to events.
265
+
266
+ Given this piece of code:
267
+
268
+ ```ruby
269
+ class CodeThatReactsToEvents
270
+ def do_something
271
+ publisher = MyPublisher.new
272
+ publisher.on(:some_event) do |variable|
273
+ return "Hello with #{variable}!"
274
+ end
275
+ publisher.execute
276
+ end
277
+ end
278
+ ```
279
+
280
+ You can test it like this:
281
+
282
+ ```ruby
283
+ require 'wisper/rspec/stub_wisper_publisher'
284
+
285
+ describe CodeThatReactsToEvents do
286
+ context "on some_event" do
287
+ before do
288
+ stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo")
289
+ end
290
+
291
+ it "renders" do
292
+ response = CodeThatReactsToEvents.new.do_something
293
+ response.should == "Hello with foo!"
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ This becomes important when testing, for example, Rails controllers in
300
+ isolation from the business logic. This technique is used at the controller
301
+ layer to isolate testing the controller from testing the encapsulated business
302
+ logic.
303
+
304
+ You can use any number of args to pass to the event:
305
+
306
+ ```ruby
307
+ stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo1", "foo2", ...)
308
+ ```
309
+
310
+ See `spec/lib/rspec_extensions_spec.rb` for a runnable example.
311
+
210
312
  ## Compatibility
211
313
 
212
314
  Tested with 1.9.x on MRI, JRuby and Rubinius.
@@ -1,7 +1,9 @@
1
1
  require "wisper/version"
2
2
  require "wisper/registration/registration"
3
3
  require "wisper/registration/object"
4
+ require "wisper/registration/object/async_listener"
4
5
  require "wisper/registration/block"
6
+ require 'wisper/global_listeners'
5
7
 
6
8
  module Wisper
7
9
  def listeners
@@ -29,8 +31,12 @@ module Wisper
29
31
 
30
32
  private
31
33
 
34
+ def all_listeners
35
+ listeners.merge(GlobalListeners.listeners)
36
+ end
37
+
32
38
  def broadcast(event, *args)
33
- listeners.each do | listener |
39
+ all_listeners.each do | listener |
34
40
  listener.broadcast(clean_event(event), *args)
35
41
  end
36
42
  end
@@ -0,0 +1,28 @@
1
+ require 'singleton'
2
+
3
+ module Wisper
4
+ class GlobalListeners
5
+ include Singleton
6
+
7
+ def initialize
8
+ @listeners = Set.new
9
+ end
10
+
11
+ def add_listener(listener, options = {})
12
+ listeners << ObjectRegistration.new(listener, options)
13
+ self
14
+ end
15
+
16
+ def listeners
17
+ @listeners
18
+ end
19
+
20
+ def self.add_listener(listener, options = {})
21
+ instance.add_listener(listener, options)
22
+ end
23
+
24
+ def self.listeners
25
+ instance.listeners
26
+ end
27
+ end
28
+ end
@@ -1,16 +1,27 @@
1
+ begin
2
+ require 'celluloid/autostart'
3
+ rescue LoadError
4
+ # no-op
5
+ end
6
+
1
7
  module Wisper
2
8
  class ObjectRegistration < Registration
3
- attr_reader :with
9
+ attr_reader :with, :async
4
10
 
5
11
  def initialize(listener, options)
6
12
  super(listener, options)
7
- @with = options[:with]
13
+ @with = options[:with]
14
+ @async = options.fetch(:async, false)
8
15
  end
9
16
 
10
17
  def broadcast(event, *args)
11
18
  method_to_call = map_event_to_method(event)
12
19
  if should_broadcast?(event) && listener.respond_to?(method_to_call)
13
- listener.public_send(method_to_call, *args)
20
+ unless async
21
+ listener.public_send(method_to_call, *args)
22
+ else
23
+ AsyncListener.new(listener, method_to_call).async.public_send(method_to_call, *args)
24
+ end
14
25
  end
15
26
  end
16
27
 
@@ -0,0 +1,23 @@
1
+ class AsyncListener
2
+ include Celluloid if defined?(Celluloid)
3
+
4
+ attr_reader :listener, :event_method
5
+
6
+ def initialize(listener, event_method)
7
+ @listener = listener
8
+ @event_method = event_method.to_sym
9
+ end
10
+
11
+ def method_missing(method, *args, &block)
12
+ if listener.respond_to?(method)
13
+ if method == event_method
14
+ listener.public_send(method, *args, &block)
15
+ terminate
16
+ else
17
+ listener.public_send(method, *args, &block)
18
+ end
19
+ else
20
+ super(method, *args, &block)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ ### Wisper Stubbing
2
+ # This is a proposal for integration as part of wisper core
3
+ # for testing: https://github.com/krisleech/wisper/issues/1
4
+ class TestWisperPublisher
5
+ include Wisper
6
+ def initialize(*args); end
7
+ end
8
+
9
+ def stub_wisper_publisher(clazz, called_method, event_to_publish, *published_event_args)
10
+ stub_const(clazz, Class.new(TestWisperPublisher) do
11
+ define_method(called_method) do
12
+ publish(event_to_publish, *published_event_args)
13
+ end
14
+ end)
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Wisper
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ class MyService
4
+ include Wisper
5
+
6
+ def execute
7
+ broadcast('success', self)
8
+ end
9
+ end
10
+
11
+ # help me...
12
+ $global = 'no'
13
+
14
+ class MyListener
15
+ def success(command)
16
+ $global = 'yes'
17
+ end
18
+ end
19
+
20
+ describe Wisper do
21
+
22
+ it 'subscribes object to all published events' do
23
+ listener = MyListener.new
24
+
25
+ command = MyService.new
26
+
27
+ command.add_listener(listener, :async => true)
28
+
29
+ command.execute
30
+ sleep(1) # seriously...
31
+ $global.should == 'yes'
32
+ end
33
+ end
34
+
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe Wisper::GlobalListeners do
4
+ let(:global_listener) { double('listener') }
5
+ let(:local_listener) { double('listener') }
6
+ let(:publisher) { Object.new.extend(Wisper) }
7
+
8
+ describe '.add_listener' do
9
+ it 'adds given listener to every publisher' do
10
+ Wisper::GlobalListeners.add_listener(global_listener)
11
+ global_listener.should_receive(:it_happened)
12
+ publisher.send(:broadcast, :it_happened)
13
+ end
14
+
15
+ it 'works along side local listeners' do
16
+ # global listener
17
+ Wisper::GlobalListeners.add_listener(global_listener)
18
+
19
+ # local listener
20
+ publisher.add_listener(local_listener)
21
+
22
+ global_listener.should_receive(:it_happened)
23
+ local_listener.should_receive(:it_happened)
24
+
25
+ publisher.send(:broadcast, :it_happened)
26
+ end
27
+ end
28
+ end
@@ -56,7 +56,9 @@ describe Wisper do
56
56
 
57
57
  it 'subscribes block can be chained' do
58
58
  insider = double('Insider')
59
+
59
60
  insider.should_receive(:render).with('success')
61
+ insider.should_receive(:render).with('failure')
60
62
 
61
63
  command = MyCommand.new
62
64
 
@@ -64,5 +66,6 @@ describe Wisper do
64
66
  .on(:failure) { |message| insider.render('failure') }
65
67
 
66
68
  command.execute(true)
69
+ command.execute(false)
67
70
  end
68
71
  end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'wisper/rspec/stub_wisper_publisher'
3
+
4
+ describe Wisper do
5
+ describe "given a piece of code invoking a publisher" do
6
+ class CodeThatReactsToEvents
7
+ def do_something
8
+ publisher = MyPublisher.new
9
+ publisher.on(:some_event) do |variable1, variable2|
10
+ return "Hello with #{variable1} #{variable2}!"
11
+ end
12
+ publisher.execute
13
+ end
14
+ end
15
+
16
+ context "when stubbing the publisher to emit an event" do
17
+ before do
18
+ stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo1", "foo2")
19
+ end
20
+
21
+ it "emits the event" do
22
+ response = CodeThatReactsToEvents.new.do_something
23
+ response.should == "Hello with foo1 foo2!"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -62,8 +62,9 @@ describe Wisper do
62
62
  end
63
63
 
64
64
  describe '.add_block_listener' do
65
+ let(:insider) { double('insider') }
66
+
65
67
  it 'subscribes given block to all events' do
66
- insider = double('insider')
67
68
  insider.should_receive(:it_happened).twice
68
69
 
69
70
  publisher.add_block_listener do
@@ -76,7 +77,6 @@ describe Wisper do
76
77
 
77
78
  describe ':on argument' do
78
79
  it '.add_block_listener subscribes block to an event' do
79
- insider = double('insider')
80
80
  insider.should_not_receive(:it_happened).once
81
81
 
82
82
  publisher.add_block_listener(:on => 'something_happened') do
@@ -20,4 +20,5 @@ Gem::Specification.new do |gem|
20
20
  gem.add_development_dependency 'rake'
21
21
  gem.add_development_dependency 'rspec'
22
22
  gem.add_development_dependency 'simplecov'
23
+ gem.add_development_dependency 'celluloid'
23
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wisper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-07 00:00:00.000000000 Z
12
+ date: 2013-05-02 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: !ruby/object:Gem::Requirement
16
+ requirement: &2152451840 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,15 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
27
- - - ! '>='
28
- - !ruby/object:Gem::Version
29
- version: '0'
24
+ version_requirements: *2152451840
30
25
  - !ruby/object:Gem::Dependency
31
26
  name: rspec
32
- requirement: !ruby/object:Gem::Requirement
27
+ requirement: &2152451300 !ruby/object:Gem::Requirement
33
28
  none: false
34
29
  requirements:
35
30
  - - ! '>='
@@ -37,15 +32,10 @@ dependencies:
37
32
  version: '0'
38
33
  type: :development
39
34
  prerelease: false
40
- version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ! '>='
44
- - !ruby/object:Gem::Version
45
- version: '0'
35
+ version_requirements: *2152451300
46
36
  - !ruby/object:Gem::Dependency
47
37
  name: simplecov
48
- requirement: !ruby/object:Gem::Requirement
38
+ requirement: &2152450760 !ruby/object:Gem::Requirement
49
39
  none: false
50
40
  requirements:
51
41
  - - ! '>='
@@ -53,12 +43,18 @@ dependencies:
53
43
  version: '0'
54
44
  type: :development
55
45
  prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
46
+ version_requirements: *2152450760
47
+ - !ruby/object:Gem::Dependency
48
+ name: celluloid
49
+ requirement: &2152450220 !ruby/object:Gem::Requirement
57
50
  none: false
58
51
  requirements:
59
52
  - - ! '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2152450220
62
58
  description: pub/sub for Ruby objects
63
59
  email:
64
60
  - kris.leech@gmail.com
@@ -74,11 +70,17 @@ files:
74
70
  - README.md
75
71
  - Rakefile
76
72
  - lib/wisper.rb
73
+ - lib/wisper/global_listeners.rb
77
74
  - lib/wisper/registration/block.rb
78
75
  - lib/wisper/registration/object.rb
76
+ - lib/wisper/registration/object/async_listener.rb
79
77
  - lib/wisper/registration/registration.rb
78
+ - lib/wisper/rspec/stub_wisper_publisher.rb
80
79
  - lib/wisper/version.rb
80
+ - spec/lib/async_spec.rb
81
+ - spec/lib/global_subscribers_spec.rb
81
82
  - spec/lib/integration_spec.rb
83
+ - spec/lib/rspec_extensions_spec.rb
82
84
  - spec/lib/simple_example_spec.rb
83
85
  - spec/lib/wisper_spec.rb
84
86
  - spec/spec_helper.rb
@@ -95,26 +97,24 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
97
  - - ! '>='
96
98
  - !ruby/object:Gem::Version
97
99
  version: '0'
98
- segments:
99
- - 0
100
- hash: -3667309302433800657
101
100
  required_rubygems_version: !ruby/object:Gem::Requirement
102
101
  none: false
103
102
  requirements:
104
103
  - - ! '>='
105
104
  - !ruby/object:Gem::Version
106
105
  version: '0'
107
- segments:
108
- - 0
109
- hash: -3667309302433800657
110
106
  requirements: []
111
107
  rubyforge_project:
112
- rubygems_version: 1.8.23
108
+ rubygems_version: 1.8.10
113
109
  signing_key:
114
110
  specification_version: 3
115
111
  summary: pub/sub for Ruby objects
116
112
  test_files:
113
+ - spec/lib/async_spec.rb
114
+ - spec/lib/global_subscribers_spec.rb
117
115
  - spec/lib/integration_spec.rb
116
+ - spec/lib/rspec_extensions_spec.rb
118
117
  - spec/lib/simple_example_spec.rb
119
118
  - spec/lib/wisper_spec.rb
120
119
  - spec/spec_helper.rb
120
+ has_rdoc: