wisper 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - '1.9.2'
4
+ - '1.9.3'
5
+ - jruby-19mode
6
+ - rbx-19mode
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Simple pub/sub for Ruby objects
4
4
 
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)
7
+
5
8
  While this is not dependent on Rails in any way it was extracted from a Rails
6
9
  project and is used as an alternative to ActiveRecord callbacks and Observers.
7
10
 
@@ -20,42 +23,123 @@ Add this line to your application's Gemfile:
20
23
 
21
24
  ## Usage
22
25
 
26
+ Any class with the Wisper module included can broadcast events to subscribed
27
+ listeners. Listeners are added, at runtime, to the publishing object.
28
+
29
+ ### Publishing
30
+
23
31
  ```ruby
24
- class CreateThing
32
+ class MyPublisher
25
33
  include Wisper
26
34
 
27
- def execute(attributes)
28
- thing = Thing.new(attributes)
29
- if thing.valid?
30
- thing.save!
31
- broadcast(:create_thing_successful, thing)
35
+ def do_something
36
+ publish(:done_something, self)
37
+ end
38
+ end
39
+ ```
40
+
41
+ When the publisher publishes an event it can pass any number of arguments which
42
+ are passed on to the listeners.
43
+
44
+ ```ruby
45
+ publish(:done_something, self, 'hello', 'world')
46
+ ```
47
+
48
+ ### Subscribing
49
+
50
+ #### Listeners
51
+
52
+ The listener is subscribed to all events it responds to.
53
+
54
+ ```ruby
55
+ listener = Object.new # any object
56
+ my_publisher = MyPublisher.new
57
+ my_publisher.subscribe(listener)
58
+ ```
59
+
60
+ #### Blocks
61
+
62
+ The block is subscribed to a single event.
63
+
64
+ ```ruby
65
+ my_publisher = MyPublisher.new
66
+ my_publisher.on(:done_something) do |publisher|
67
+ # ...
68
+ end
69
+ ```
70
+
71
+ ### ActiveRecord
72
+
73
+ ```ruby
74
+ class Post < ActiveRecord::Base
75
+ include Wisper
76
+
77
+ def create
78
+ if save
79
+ publish(:create_post_successful, self)
32
80
  else
33
- broadcast(:create_thing_failed, thing)
81
+ publish(:create_post_failed, self)
34
82
  end
35
83
  end
36
84
  end
85
+ ```
86
+
87
+ ### ActionController
37
88
 
38
- class ThingsController < ApplicationController
89
+ ```ruby
90
+ class PostsController < ApplicationController
39
91
  def create
40
- command = CreateThing.new
92
+ @post = Post.new(params[:post])
41
93
 
42
- command.add_listener(PusherListener.new)
43
- command.add_listener(ActivityListener.new)
44
- command.add_listener(StatisticsListener.new)
94
+ @post.subscribe(PusherListener.new)
95
+ @post.subscribe(ActivityListener.new)
96
+ @post.subscribe(StatisticsListener.new)
45
97
 
46
- command.respond_to(:create_thing_successful) do |thing|
47
- redirect_to thing
48
- end
98
+ @post.on(:create_post_successful) { |post| redirect_to post }
99
+ @post.on(:create_post_failed) { |post| render :action => :new }
49
100
 
50
- command.respond_to(:create_thing_failed) do |thing|
51
- @thing = thing
52
- render :action => :new
53
- end
101
+ @post.create
102
+ end
103
+ end
104
+ ```
105
+
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.
54
110
 
55
- command.execute(params[:thing])
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.
116
+
117
+ ```ruby
118
+ class CreateThing
119
+ include Wisper
120
+
121
+ def execute(attributes)
122
+ thing = Thing.new(attributes)
123
+
124
+ if thing.valid?
125
+ ActiveRecord::Base.transaction do
126
+ thing.save
127
+ # ...
128
+ end
129
+ publish(:create_thing_successful, thing)
130
+ else
131
+ publish(:create_thing_failed, thing)
132
+ end
56
133
  end
57
134
  end
135
+ ```
136
+
137
+ ### Example listeners
58
138
 
139
+ These are typical app wide listeners which have a method for pretty much every
140
+ event which is broadcast.
141
+
142
+ ```ruby
59
143
  class PusherListener
60
144
  def create_thing_successful(thing)
61
145
  # ...
@@ -75,6 +159,59 @@ class StatisticsListener
75
159
  end
76
160
  ```
77
161
 
162
+ ## Subscribing to selected events
163
+
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`.
167
+
168
+ ```ruby
169
+ post_creater.subscribe(PusherListener.new, :on => :create_post_successful)
170
+ ```
171
+
172
+ ## Mapping event to a different method
173
+
174
+ By default the method called on the subscriber is the same as the event
175
+ broadcast. However it can be mapped to a different method using `:with`.
176
+
177
+ ```ruby
178
+ report_creator.subscribe(MailResponder.new, :with => :successful)
179
+ ```
180
+
181
+ In the above case it is pretty useless unless used in conjuction with `:on`
182
+ since all events will get mapped to `:successful`. Instead you might do
183
+ something like this:
184
+
185
+ ```ruby
186
+ report_creator.subscribe(MailResponder.new, :on => :create_report_successful,
187
+ :with => :successful)
188
+ ```
189
+
190
+ If you pass an array of events to `:on` each event will be mapped to the same
191
+ method when `:with` is specified. If you need to listen for select events
192
+ _and_ map each one to a different method subscribe the listener once for
193
+ each mapping:
194
+
195
+ ```ruby
196
+ report_creator.subscribe(MailResponder.new, :on => :create_report_successful,
197
+ :with => :successful)
198
+
199
+ report_creator.subscribe(MailResponder.new, :on => :create_report_failed,
200
+ :with => :failed)
201
+ ```
202
+
203
+ ## Chaining subscriptions
204
+
205
+ ```ruby
206
+ post.on(:success) { |post| redirect_to post }
207
+ .on(:failure) { |post| render :action => :edit, :locals => :post => post }
208
+ ```
209
+
210
+ ## Compatibility
211
+
212
+ Tested with 1.9.x on MRI, JRuby and Rubinius.
213
+ See the [build status](https://travis-ci.org/krisleech/wisper) for details.
214
+
78
215
  ## License
79
216
 
80
217
  (The MIT License)
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
6
+
7
+ task :default => :spec
data/lib/wisper.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  require "wisper/version"
2
+ require "wisper/registration/registration"
3
+ require "wisper/registration/object"
4
+ require "wisper/registration/block"
2
5
 
3
6
  module Wisper
4
7
  def listeners
@@ -7,56 +10,34 @@ module Wisper
7
10
 
8
11
  def add_listener(listener, options = {})
9
12
  listeners << ObjectRegistration.new(listener, options)
13
+ self
10
14
  end
11
15
 
16
+ alias :subscribe :add_listener
17
+
12
18
  def add_block_listener(options = {}, &block)
13
19
  listeners << BlockRegistration.new(block, options)
20
+ self
14
21
  end
15
22
 
16
23
  # sugar
17
24
  def respond_to(event, &block)
18
- listeners << BlockRegistration.new(block, :on => event)
19
- end
20
-
21
- class BlockRegistration
22
- attr_reader :on, :listener
23
-
24
- def initialize(block, options)
25
- @listener = block
26
- @on = Array(options.fetch(:on) { 'all' }).map(&:to_s)
27
- end
28
-
29
- def broadcast(event, *args)
30
- if on.include?(event) || on.include?('all')
31
- listener.call(*args)
32
- end
33
- end
25
+ add_block_listener({:on => event}, &block)
34
26
  end
35
27
 
36
- class ObjectRegistration
37
- attr_reader :on, :listener
38
-
39
- def initialize(listener, options)
40
- @listener = listener
41
- @method = options[:method]
42
- @on = Array(options.fetch(:on) { 'all' }).map(&:to_s)
43
- end
44
-
45
- def broadcast(event, *args)
46
- if (on.include?(event) || on.include?('all')) && listener.respond_to?(event)
47
- listener.public_send(event, *args)
48
- end
49
- end
50
- end
28
+ alias :on :respond_to
51
29
 
52
30
  private
53
31
 
54
32
  def broadcast(event, *args)
55
33
  listeners.each do | listener |
56
- listener.broadcast(clean_event(event), *args)
34
+ listener.broadcast(clean_event(event), *args)
57
35
  end
58
36
  end
59
37
 
38
+ alias :publish :broadcast
39
+ alias :announce :broadcast
40
+
60
41
  def clean_event(event)
61
42
  event.to_s.gsub('-', '_')
62
43
  end
@@ -0,0 +1,9 @@
1
+ module Wisper
2
+ class BlockRegistration < Registration
3
+ def broadcast(event, *args)
4
+ if should_broadcast?(event)
5
+ listener.call(*args)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Wisper
2
+ class ObjectRegistration < Registration
3
+ attr_reader :with
4
+
5
+ def initialize(listener, options)
6
+ super(listener, options)
7
+ @with = options[:with]
8
+ end
9
+
10
+ def broadcast(event, *args)
11
+ method_to_call = map_event_to_method(event)
12
+ if should_broadcast?(event) && listener.respond_to?(method_to_call)
13
+ listener.public_send(method_to_call, *args)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def map_event_to_method(event)
20
+ with || event
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Wisper
2
+ class Registration
3
+ attr_reader :on, :listener
4
+
5
+ def initialize(listener, options)
6
+ @listener = listener
7
+ @on = Array(options.fetch(:on) { 'all' }).map(&:to_s)
8
+ end
9
+
10
+ private
11
+
12
+ def should_broadcast?(event)
13
+ on.include?(event) || on.include?('all')
14
+ end
15
+ end
16
+ end
@@ -1,3 +1,3 @@
1
1
  module Wisper
2
- VERSION = "0.0.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -38,4 +38,31 @@ describe Wisper do
38
38
 
39
39
  command.execute(true)
40
40
  end
41
+
42
+ it 'maps events to different methods' do
43
+ listener_1 = double('listener')
44
+ listener_2 = double('listener')
45
+ listener_1.should_receive(:happy_days).with('hello')
46
+ listener_2.should_receive(:sad_days).with('world')
47
+
48
+ command = MyCommand.new
49
+
50
+ command.add_listener(listener_1, :on => :success, :with => :happy_days)
51
+ command.add_listener(listener_2, :on => :failure, :with => :sad_days)
52
+
53
+ command.execute(true)
54
+ command.execute(false)
55
+ end
56
+
57
+ it 'subscribes block can be chained' do
58
+ insider = double('Insider')
59
+ insider.should_receive(:render).with('success')
60
+
61
+ command = MyCommand.new
62
+
63
+ command.on(:success) { |message| insider.render('success') }
64
+ .on(:failure) { |message| insider.render('failure') }
65
+
66
+ command.execute(true)
67
+ end
41
68
  end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ class MyPublisher
4
+ include Wisper
5
+
6
+ def do_something
7
+ # ...
8
+ broadcast(:bar, self)
9
+ broadcast(:foo, self)
10
+ end
11
+ end
12
+
13
+ describe 'simple publishing' do
14
+ it 'subscribes listener to events' do
15
+ listener = double('listener')
16
+ listener.should_receive(:foo).with instance_of MyPublisher
17
+ listener.should_receive(:bar).with instance_of MyPublisher
18
+
19
+ my_publisher = MyPublisher.new
20
+ my_publisher.add_listener(listener)
21
+ my_publisher.do_something
22
+ end
23
+ end
@@ -5,7 +5,7 @@ describe Wisper do
5
5
  let(:publisher) { Object.new.extend(Wisper) }
6
6
 
7
7
  describe '.add_listener' do
8
- it 'subscribes listener to all published events' do
8
+ it 'subscribes given listener to all published events' do
9
9
  listener.should_receive(:this_happened)
10
10
  listener.should_receive(:so_did_this)
11
11
 
@@ -15,52 +15,126 @@ describe Wisper do
15
15
  publisher.send(:broadcast, 'so_did_this')
16
16
  end
17
17
 
18
- it 'subscribes listener to selected events' do
19
- listener.should_receive(:this_happened)
20
- listener.stub(:so_did_this)
21
- listener.should_not_receive(:so_did_this)
18
+ describe ':on argument' do
19
+ it 'subscribes given listener to a single event' do
20
+ listener.should_receive(:this_happened)
21
+ listener.stub(:so_did_this)
22
+ listener.should_not_receive(:so_did_this)
22
23
 
23
- listener.respond_to?(:so_did_this).should be_true
24
+ listener.respond_to?(:so_did_this).should be_true
24
25
 
25
- publisher.add_listener(listener, :on => 'this_happened')
26
+ publisher.add_listener(listener, :on => 'this_happened')
26
27
 
27
- publisher.send(:broadcast, 'this_happened')
28
- publisher.send(:broadcast, 'so_did_this')
28
+ publisher.send(:broadcast, 'this_happened')
29
+ publisher.send(:broadcast, 'so_did_this')
30
+ end
31
+
32
+ it 'subscribes given listener to many events' do
33
+ listener.should_receive(:this_happened)
34
+ listener.should_receive(:and_this)
35
+ listener.stub(:so_did_this)
36
+ listener.should_not_receive(:so_did_this)
37
+
38
+ listener.respond_to?(:so_did_this).should be_true
39
+
40
+ publisher.add_listener(listener, :on => ['this_happened', 'and_this'])
41
+
42
+ publisher.send(:broadcast, 'this_happened')
43
+ publisher.send(:broadcast, 'so_did_this')
44
+ publisher.send(:broadcast, 'and_this')
45
+ end
29
46
  end
30
47
 
31
- it 'does not receive events it can not respond to' do
32
- listener.should_not_receive(:so_did_this)
33
- listener.stub(:respond_to?, false)
48
+ describe ':with argument' do
49
+ it 'sets method to call listener with on event' do
50
+ listener.should_receive(:different_method).twice
34
51
 
35
- listener.respond_to?(:so_did_this).should be_false
52
+ publisher.add_listener(listener, :with => :different_method)
36
53
 
37
- publisher.add_listener(listener, :on => 'so_did_this')
54
+ publisher.send(:broadcast, 'this_happened')
55
+ publisher.send(:broadcast, 'so_did_this')
56
+ end
57
+ end
38
58
 
39
- publisher.send(:broadcast, 'so_did_this')
59
+ it 'returns publisher so methods can be chained' do
60
+ publisher.add_listener(listener, :on => 'so_did_this').should == publisher
40
61
  end
41
62
  end
42
63
 
43
- describe 'Block listeners' do
44
- it 'subscribes block to all events' do
64
+ describe '.add_block_listener' do
65
+ it 'subscribes given block to all events' do
45
66
  insider = double('insider')
46
- insider.should_receive(:it_happened)
67
+ insider.should_receive(:it_happened).twice
47
68
 
48
69
  publisher.add_block_listener do
49
70
  insider.it_happened
50
71
  end
51
72
 
52
73
  publisher.send(:broadcast, 'something_happened')
74
+ publisher.send(:broadcast, 'and_so_did_this')
53
75
  end
54
76
 
55
- it 'subscribes block to selected events' do
56
- insider = double('insider')
57
- insider.should_not_receive(:it_happened)
77
+ describe ':on argument' do
78
+ it '.add_block_listener subscribes block to an event' do
79
+ insider = double('insider')
80
+ insider.should_not_receive(:it_happened).once
58
81
 
82
+ publisher.add_block_listener(:on => 'something_happened') do
83
+ insider.it_happened
84
+ end
85
+
86
+ publisher.send(:broadcast, 'something_happened')
87
+ publisher.send(:broadcast, 'and_so_did_this')
88
+ end
89
+ end
90
+
91
+ it 'returns publisher so methods can be chained' do
59
92
  publisher.add_block_listener(:on => 'this_thing_happened') do
93
+ end.should == publisher
94
+ end
95
+ end
96
+
97
+ describe '.on (alternative block syntax)' do
98
+ it 'subscribes given block to an event' do
99
+ insider = double('insider')
100
+ insider.should_receive(:it_happened)
101
+
102
+ publisher.on(:something_happened) do
60
103
  insider.it_happened
61
104
  end
62
105
 
63
106
  publisher.send(:broadcast, 'something_happened')
64
107
  end
65
108
  end
109
+
110
+ describe '.broadcast' do
111
+ it 'does not publish events which cannot be responded to' do
112
+ listener.should_not_receive(:so_did_this)
113
+ listener.stub(:respond_to?, false)
114
+
115
+ publisher.add_listener(listener, :on => 'so_did_this')
116
+
117
+ publisher.send(:broadcast, 'so_did_this')
118
+ end
119
+
120
+ describe ':event argument' do
121
+ it 'is indifferent to string and symbol' do
122
+ listener.should_receive(:this_happened).twice
123
+
124
+ publisher.add_listener(listener)
125
+
126
+ publisher.send(:broadcast, 'this_happened')
127
+ publisher.send(:broadcast, :this_happened)
128
+ end
129
+
130
+ it 'is indifferent to dasherized and underscored strings' do
131
+ listener.should_receive(:this_happened).twice
132
+
133
+ publisher.add_listener(listener)
134
+
135
+ publisher.send(:broadcast, 'this_happened')
136
+ publisher.send(:broadcast, 'this-happened')
137
+ end
138
+ end
139
+ end
66
140
  end
data/wisper.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
10
10
  gem.email = ["kris.leech@gmail.com"]
11
11
  gem.description = %q{pub/sub for Ruby objects}
12
12
  gem.summary = %q{pub/sub for Ruby objects}
13
- gem.homepage = ""
13
+ gem.homepage = "https://github.com/krisleech/wisper"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
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: 0.0.2
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-30 00:00:00.000000000 Z
12
+ date: 2013-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -68,17 +68,22 @@ extra_rdoc_files: []
68
68
  files:
69
69
  - .gitignore
70
70
  - .rspec
71
+ - .travis.yml
71
72
  - Gemfile
72
73
  - Guardfile
73
74
  - README.md
74
75
  - Rakefile
75
76
  - lib/wisper.rb
77
+ - lib/wisper/registration/block.rb
78
+ - lib/wisper/registration/object.rb
79
+ - lib/wisper/registration/registration.rb
76
80
  - lib/wisper/version.rb
77
81
  - spec/lib/integration_spec.rb
82
+ - spec/lib/simple_example_spec.rb
78
83
  - spec/lib/wisper_spec.rb
79
84
  - spec/spec_helper.rb
80
85
  - wisper.gemspec
81
- homepage: ''
86
+ homepage: https://github.com/krisleech/wisper
82
87
  licenses: []
83
88
  post_install_message:
84
89
  rdoc_options: []
@@ -92,7 +97,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
97
  version: '0'
93
98
  segments:
94
99
  - 0
95
- hash: 1275527942572696193
100
+ hash: -3667309302433800657
96
101
  required_rubygems_version: !ruby/object:Gem::Requirement
97
102
  none: false
98
103
  requirements:
@@ -101,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
106
  version: '0'
102
107
  segments:
103
108
  - 0
104
- hash: 1275527942572696193
109
+ hash: -3667309302433800657
105
110
  requirements: []
106
111
  rubyforge_project:
107
112
  rubygems_version: 1.8.23
@@ -110,5 +115,6 @@ specification_version: 3
110
115
  summary: pub/sub for Ruby objects
111
116
  test_files:
112
117
  - spec/lib/integration_spec.rb
118
+ - spec/lib/simple_example_spec.rb
113
119
  - spec/lib/wisper_spec.rb
114
120
  - spec/spec_helper.rb