wisper 0.0.2 → 1.0.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.
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