eventually 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ coverage
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Eventually
2
2
 
3
- `Eventually` is a module that facilitates evented callback management *similar* to the [EventEmitter API](http://nodejs.org/docs/v0.4.7/api/events.html) in NodeJS. Support for Ruby's various lambda-ish callback styles is heavily baked in, so using blocks, lambdas, procs, or event detached methods works out of the box, batteries included. Simply include `Eventually` in the class you will be emitting events from, register some listeners and fire away.
3
+ `Eventually` is a module that facilitates evented callback management *similar* to the [EventEmitter API](http://nodejs.org/docs/v0.4.7/api/events.html) in NodeJS. Support for Ruby's various callback styles is heavily baked in, so using blocks, lambdas, procs, or even detached methods works out of the box, batteries included. Simply include `Eventually` in the class you will be emitting events from, register some listeners and fire away.
4
4
 
5
5
  ```ruby
6
6
  class Car
@@ -36,7 +36,7 @@ The previous snippet acts mostly as documentation.
36
36
 
37
37
  ## Callback arity validation
38
38
 
39
- However, sometimes you want to be sure that a given registered callback will conform to your event interface, so specify an **arity validation**. Let's add another event to our definition and ensure that callbacks registering for that event must have an arity of 1.
39
+ However, sometimes you want to be sure that a given registered callback will conform to your event interface, so specify an **arity validation**. Let's add another event to our definition and ensure that callbacks registering for that event must have an arity of 1. This will also raise an error if you attempt to emit an event with the wrong argument arity.
40
40
 
41
41
  ```ruby
42
42
  class Car
@@ -74,6 +74,7 @@ car = Car.new
74
74
  car.on(:turning) do
75
75
  puts 'the car is turning'
76
76
  end
77
+ ```
77
78
 
78
79
  *See the **examples/scrict.rb** file for more on this.*
79
80
 
data/eventually.gemspec CHANGED
@@ -20,4 +20,6 @@ Gem::Specification.new do |s|
20
20
 
21
21
  # specify any dependencies here; for example:
22
22
  s.add_development_dependency "rspec"
23
+ s.add_development_dependency "yard"
24
+ s.add_development_dependency "simplecov"
23
25
  end
data/lib/eventually.rb CHANGED
@@ -1,4 +1,8 @@
1
- require "eventually/version"
1
+ require 'eventually/version'
2
+ require 'eventually/event'
3
+ require 'eventually/callable'
4
+ require 'eventually/validation/arity'
5
+ require 'eventually/validation/max_listeners'
2
6
 
3
7
  # Eventually is a module that facilitates evented callback
4
8
  # management similar to the EventEmitter API in NodeJS.
@@ -11,10 +15,13 @@ require "eventually/version"
11
15
  module Eventually
12
16
  def self.included(base)
13
17
  base.extend(ClassMethods)
18
+ base.emits(:listener_added, :arity => 1)
14
19
  end
15
20
 
16
21
  module ClassMethods
17
- NO_CHECK_ARITY = -1
22
+ NO_CHECK_ARITY = -2
23
+ DEFAULT_MAX_LISTENERS = 10
24
+
18
25
  attr_accessor :emittable_events
19
26
 
20
27
  # Define an event or list of events
@@ -47,6 +54,19 @@ module Eventually
47
54
  emittable.key?(evt.to_sym)
48
55
  end
49
56
 
57
+ # Return the maximum number of listeners before
58
+ # we'll start printing memory warnings.
59
+ # Default max is 10
60
+ def max_listeners
61
+ @max_listeners ||= DEFAULT_MAX_LISTENERS
62
+ end
63
+
64
+ # Set the maximum listener number. Setting this max
65
+ # to 0 indicates unlimited listeners allowed.
66
+ def max_listeners= max
67
+ @max_listeners = max
68
+ end
69
+
50
70
  # Puts instances into strict mode. This does two things:
51
71
  # - Raise an error if registering a callback for an event
52
72
  # that has not already been pre-defined (e.g. with #emits)
@@ -62,7 +82,7 @@ module Eventually
62
82
  @strict = false
63
83
  end
64
84
 
65
- # Are we strict or not
85
+ # Report on strict mode
66
86
  def strict?
67
87
  @strict || false
68
88
  end
@@ -86,9 +106,10 @@ module Eventually
86
106
 
87
107
  # Shorthand predicate to determine if a given event is
88
108
  # "emittable" or "registerable"
89
- def can_emit_or_register?(event)
109
+ def emittable?(event)
90
110
  !strict? || emits?(event)
91
111
  end
112
+ alias :registerable? :emittable?
92
113
 
93
114
  private
94
115
 
@@ -102,24 +123,26 @@ module Eventually
102
123
  #
103
124
  # Usage: see Eventually#emit or examples directory
104
125
  #
105
- def on(event, callable=nil, &blk)
106
- raise "Event type :#{event} will not be emitted. Use #{self.class.name}.emits(:#{event})" unless self.class.can_emit_or_register?(event)
107
-
108
- cbk = nil
109
- if callable.respond_to?(:call)
110
- cbk = callable
111
- elsif block_given? && !blk.nil?
112
- cbk = blk
113
- else
114
- raise 'Cannot register callback. Neither callable nor block was given'
115
- end
126
+ def on(evt_name, callable=nil, &blk)
127
+ evt_name = evt_name.to_sym unless evt_name.is_a?(Symbol)
116
128
 
117
- # if self.class.validates_arity?(event) && cbk.arity != (expected_arity = self.class.arity_for_event(event))
118
- unless valid_event_arity?(event, cbk.arity)
119
- raise "Invalid callback arity for event :#{event} (expected #{self.class.arity_for_event(event)}, received #{cbk.arity})"
120
- end
129
+ cb = Eventually::Callable.new(callable, blk)
130
+ Eventually::Validation::Arity.new(evt_name, self, cb.arity).raise_unless_valid!
131
+
132
+ event = get_event(evt_name)
133
+ event.add_callable(cb)
134
+ emit(:listener_added, cb)
121
135
 
122
- (__registered__[event.to_sym] ||= []) << cbk
136
+ Eventually::Validation::MaxListeners.new(self).warn_unless_valid!
137
+ [event, cb]
138
+ end
139
+
140
+ # Event registration method which will remove the given
141
+ # callback after it is invoked. See Eventually#on for registration details.
142
+ def once(event, callable=nil, &blk)
143
+ event, cb = on(event, callable, &blk)
144
+ cb.availability = :once
145
+ [event, cb]
123
146
  end
124
147
 
125
148
  # Emit the payload arguments back to the registered listeners
@@ -141,29 +164,46 @@ module Eventually
141
164
  # end
142
165
  # car.stop # this will indirectly invoke the above callback
143
166
  #
144
- def emit(event, *payload)
145
- raise "Event type :#{event} cannot be emitted. Use #{self.class.name}.emits(:#{event})" unless self.class.can_emit_or_register?(event)
146
-
147
- unless valid_event_arity?(event, payload.length)
148
- raise "Invalid emit arity for event :#{event} (expected #{self.class.arity_for_event(event)}, received #{payload.length})"
149
- end
150
-
151
- (__registered__[event.to_sym] || []).each do |cbk|
152
- cbk.call(*payload)
153
- end
167
+ def emit(evt_name, *payload)
168
+ evt_name = evt_name.to_sym unless evt_name.is_a?(Symbol)
169
+ event = get_event(evt_name)
170
+ Eventually::Validation::Arity.new(evt_name, self, payload.length).raise_unless_valid!
171
+ event.emit(*payload)
172
+ end
173
+
174
+ # Report the number of listeners across all registered events
175
+ def num_listeners
176
+ _events.values.inject(0){|acc, evt| acc + evt.callables.size}
177
+ end
178
+
179
+ # Report the number of registered listeners for the given event
180
+ def listeners(event)
181
+ get_event(event).callables.map(&:callable)
154
182
  end
155
183
 
156
- # Shorthand predicate to determine if the given cbk for this event
157
- # has a valid arity amount
158
- def valid_event_arity?(event, arity_count)
159
- expected_arity = self.class.arity_for_event(event)
160
- !self.class.validates_arity?(event) || arity_count == expected_arity
184
+ # Remove the given listener callback from the given event callback list
185
+ def remove_listener(event, cbk)
186
+ get_event(event).remove_callable(cbk)
187
+ end
188
+
189
+ # Remove all listener callbacks for the given event
190
+ def remove_all_listeners(event)
191
+ get_event(event).remove_all_callables
161
192
  end
162
193
 
163
194
  private
164
195
 
165
- def __registered__
166
- @__registered__ ||= {}
196
+ def _events
197
+ @_events ||= {}
198
+ end
199
+
200
+ def get_event(event)
201
+ evt = _events[event]
202
+ unless evt
203
+ evt = Eventually::Event.new(event, self.class.registerable?(event))
204
+ _events[event] = evt
205
+ end
206
+ evt
167
207
  end
168
208
 
169
209
  end
@@ -0,0 +1,34 @@
1
+ require 'forwardable'
2
+ require 'eventually/validation/arity'
3
+
4
+ module Eventually
5
+ class Callable
6
+ extend Forwardable
7
+ attr_reader :callable
8
+ attr_accessor :availability
9
+
10
+ delegate [:arity, :call, :to_proc] => :@callable
11
+
12
+ def initialize(callable, block, availability=:continuous)
13
+ @callable = pick_callable(callable, block)
14
+ @availability = availability
15
+ end
16
+
17
+ def pick_callable(c, b)
18
+ cb = nil
19
+ if c.respond_to?(:call)
20
+ cb = c
21
+ elsif !b.nil?
22
+ cb = b
23
+ else
24
+ raise 'Cannot register callable. Neither callable nor block was given.'
25
+ end
26
+ end
27
+
28
+ def continuous?
29
+ @availability == :continuous
30
+ end
31
+
32
+ end
33
+ end
34
+
@@ -0,0 +1,47 @@
1
+ require 'eventually'
2
+ require 'eventually/callable'
3
+
4
+ module Eventually
5
+ class Event
6
+ attr_reader :name, :callables
7
+
8
+ def initialize(name, emittable=true)
9
+ @name = name.to_sym
10
+ @emittable = !!emittable
11
+ @callables = []
12
+ end
13
+
14
+ def add_callable(callable)
15
+ raise "Event type :#{name} will not be emitted." unless emittable?
16
+ @callables << callable if callable_valid?(callable)
17
+ end
18
+
19
+ def remove_callable(callable_to_remove)
20
+ if callable_valid?(callable_to_remove)
21
+ delete_handler = proc{|callable| callable == callable_to_remove }
22
+ else
23
+ delete_handler = proc{|callable| callable.callable == callable_to_remove }
24
+ end
25
+ @callables.delete_if(&delete_handler)
26
+ end
27
+
28
+ def remove_all_callables
29
+ @callables = []
30
+ end
31
+
32
+ def emit(*payload)
33
+ raise "Event type :#{name} cannot be emitted." unless emittable?
34
+ @callables.each {|callable| callable.call(*payload) }
35
+ @callables.delete_if {|callable| !callable.continuous? }
36
+ end
37
+
38
+ def callable_valid?(callable)
39
+ callable.is_a?(Eventually::Callable)
40
+ end
41
+
42
+ def emittable?
43
+ @emittable
44
+ end
45
+ alias :registerable? :emittable?
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ require 'eventually/callable'
2
+ module Eventually
3
+ module Validation
4
+ class Arity
5
+ attr_reader :expected, :received
6
+ def initialize(event, emitter, arity)
7
+ @event = event
8
+ @emitter = emitter
9
+ @arity = @received = arity
10
+ @expected = @emitter.class.arity_for_event(@event)
11
+ end
12
+
13
+ def valid?
14
+ !@emitter.class.validates_arity?(@event) || @received == @expected
15
+ end
16
+
17
+ def raise_unless_valid!
18
+ raise "Arity validation failed for event :#{@event} (expected #{@expected}, received #{@received})" unless valid?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Eventually
2
+ module Validation
3
+ class MaxListeners
4
+ def initialize(emitter)
5
+ @emitter = emitter
6
+ end
7
+
8
+ def valid?
9
+ @emitter.class.max_listeners == 0 || @emitter.num_listeners <= @emitter.class.max_listeners
10
+ end
11
+
12
+ def warn_unless_valid!
13
+ puts "Warning: #{@emitter.class.name} has more than #{@emitter.class.max_listeners} registered listeners." unless valid?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Eventually
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ require 'eventually/callable'
3
+
4
+ describe Eventually::Callable do
5
+ let(:cb_lambda) { lambda{ :lambda } }
6
+ let(:cb_proc) { proc{ :proc } }
7
+ let(:cb_block) { proc{ :block } }
8
+ let(:cb_method) { def callable_meth; :meth end; method(:callable_meth) }
9
+ let(:availability) { :continuous }
10
+
11
+ subject { Eventually::Callable.new(cb_lambda, cb_block, availability) }
12
+ its(:callable) { should eq cb_lambda }
13
+ its(:availability) { should eq availability }
14
+
15
+ context 'when callable arg is' do
16
+ context 'a lambda' do
17
+ subject { Eventually::Callable.new(cb_lambda, nil) }
18
+ its(:callable) { should eq cb_lambda }
19
+ end
20
+ context 'a proc' do
21
+ subject { Eventually::Callable.new(cb_proc, nil) }
22
+ its(:callable) { should eq cb_proc }
23
+ end
24
+ context 'a detached method' do
25
+ subject { Eventually::Callable.new(cb_method, nil) }
26
+ its(:callable) { should eq cb_method }
27
+ end
28
+ end
29
+
30
+ context 'when callable arg is not valid' do
31
+ context 'and block arg is valid' do
32
+ subject { Eventually::Callable.new(nil, cb_block) }
33
+ its(:callable) { should eq cb_block }
34
+ end
35
+
36
+ context 'and block arg is valid' do
37
+ it 'raises an error that provided callable(s) were invalid' do
38
+ expect {
39
+ Eventually::Callable.new(nil, nil)
40
+ }.should raise_error(/Cannot register callable\. Neither callable nor block was given\./)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#continuous?' do
46
+ context 'when availability is :continuous' do
47
+ subject { Eventually::Callable.new(cb_lambda, nil, :continuous) }
48
+ its(:availability) { should eq :continuous }
49
+ its(:continuous?) { should be_true }
50
+ end
51
+
52
+ context 'when availability is :continuous' do
53
+ subject { Eventually::Callable.new(cb_lambda, nil, :once) }
54
+ its(:availability) { should eq :once }
55
+ its(:continuous?) { should be_false }
56
+ end
57
+ end
58
+
59
+ context 'delegation' do
60
+ it 'delegates :arity to callable' do
61
+ subject.arity.should eq subject.callable.arity
62
+ end
63
+ it 'delegates :call to callable' do
64
+ subject.call.should eq subject.callable.call
65
+ end
66
+ it 'delegates :to_proc to callable' do
67
+ subject.to_proc.should eq subject.callable.to_proc
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+ require 'eventually/event'
3
+
4
+ describe Eventually::Event do
5
+ let(:event_name) { :started }
6
+ let(:callable) { Eventually::Callable.new(lambda{}, nil) }
7
+
8
+ subject { Eventually::Event.new(event_name) }
9
+ its(:name) { should eq event_name }
10
+ its(:emittable?) { should be_true }
11
+ its(:callables) { should be_instance_of(Array) }
12
+ its(:callables) { should be_empty }
13
+
14
+ describe '#add_callable' do
15
+ it 'adds a callable object to the callables array' do
16
+ subject.add_callable(callable)
17
+ subject.callables.should eq [callable]
18
+ end
19
+
20
+ it 'skips adding invalid callable objects to callables array' do
21
+ subject.add_callable(nil)
22
+ subject.add_callable(1)
23
+ subject.add_callable("hello, world")
24
+ subject.callables.should be_empty
25
+ end
26
+
27
+ context 'when event is not emittable' do
28
+ it 'raises an error' do
29
+ evt = Eventually::Event.new(event_name, false)
30
+ expect { evt.add_callable(callable) }.to raise_error(/Event type :#{event_name} will not be emitted\./)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#remove_callable' do
36
+ it 'removes raw callable' do
37
+ evt = Eventually::Event.new(event_name)
38
+ raw_callable = lambda{}
39
+ callable = Eventually::Callable.new(raw_callable, nil)
40
+ evt.add_callable(callable)
41
+ evt.callables.should have(1).item
42
+ evt.remove_callable(raw_callable)
43
+ evt.callables.should be_empty
44
+ end
45
+
46
+ it 'removes wrapped raw callable' do
47
+ evt = Eventually::Event.new(event_name)
48
+ callable = Eventually::Callable.new(lambda{}, nil)
49
+ evt.add_callable(callable)
50
+ evt.callables.should have(1).item
51
+ evt.remove_callable(callable)
52
+ evt.callables.should be_empty
53
+ end
54
+ end
55
+
56
+ describe '#remove_all_callables' do
57
+ it 'clears out all registered callables' do
58
+ evt = Eventually::Event.new(event_name)
59
+ evt.add_callable(Eventually::Callable.new(lambda{}, nil))
60
+ evt.add_callable(Eventually::Callable.new(lambda{}, nil))
61
+ evt.add_callable(Eventually::Callable.new(lambda{}, nil))
62
+ evt.add_callable(Eventually::Callable.new(lambda{}, nil))
63
+ evt.add_callable(Eventually::Callable.new(lambda{}, nil))
64
+ evt.callables.should have(5).items
65
+ evt.remove_all_callables
66
+ evt.callables.should be_empty
67
+ end
68
+ end
69
+
70
+ describe '#emit' do
71
+ context 'when not emittable' do
72
+ it 'raises an error' do
73
+ evt = Eventually::Event.new(event_name, false)
74
+ expect { evt.emit(:data) }.to raise_error(/Event type :#{event_name} cannot be emitted\./)
75
+ end
76
+ end
77
+
78
+ it 'calls each listener callback' do
79
+ evt = Eventually::Event.new(event_name)
80
+ cbs = [
81
+ Eventually::Callable.new(lambda{}, nil),
82
+ Eventually::Callable.new(lambda{}, nil),
83
+ Eventually::Callable.new(lambda{}, nil),
84
+ Eventually::Callable.new(lambda{}, nil),
85
+ Eventually::Callable.new(lambda{}, nil)
86
+ ]
87
+ cbs.each do |cb|
88
+ evt.add_callable(cb)
89
+ cb.should_receive(:call).once.with(:data)
90
+ end
91
+ evt.emit(:data)
92
+ end
93
+
94
+ context 'when some callables are one-time callable' do
95
+ it 'removes the one-time callable callables from the callables list' do
96
+ evt = Eventually::Event.new(event_name)
97
+ cbs = [
98
+ Eventually::Callable.new(lambda{}, nil),
99
+ Eventually::Callable.new(lambda{}, nil, :once),
100
+ Eventually::Callable.new(lambda{}, nil, :once),
101
+ Eventually::Callable.new(lambda{}, nil, :once),
102
+ Eventually::Callable.new(lambda{}, nil)
103
+ ]
104
+ cbs.each do |cb|
105
+ evt.add_callable(cb)
106
+ cb.should_receive(:call).once.with(:data)
107
+ end
108
+ evt.callables.should have(5).items
109
+ evt.emit(:data)
110
+ evt.callables.should have(2).items
111
+ end
112
+ end
113
+ end
114
+
115
+ end
@@ -8,12 +8,27 @@ describe Eventually do
8
8
  before(:each) do
9
9
  Emitter.disable_strict!
10
10
  Emitter.emits_none
11
+ Emitter.emits(:listener_added, :arity => 1)
12
+ Emitter.max_listeners = 10
11
13
  end
12
14
 
13
15
  let(:emitter) { Emitter.new }
14
16
  let(:defined_events) { [:one, :two, :three] }
15
17
 
16
18
  context 'external api' do
19
+ describe '.max_listeners' do
20
+ it 'returns the number of listeners allowed before memory warnings are printed' do
21
+ Emitter.max_listeners.should eq 10
22
+ end
23
+ end
24
+
25
+ describe '.max_listeners=' do
26
+ it 'sets the max number of listeners to allow before printing memory warnings' do
27
+ Emitter.max_listeners = 20
28
+ Emitter.max_listeners.should eq 20
29
+ end
30
+ end
31
+
17
32
  describe '.emits_none' do
18
33
  it 'clears out emitter definitions' do
19
34
  Emitter.emits(:jigger)
@@ -36,7 +51,7 @@ describe Eventually do
36
51
 
37
52
  it 'provides a list of pre-defined emittable events' do
38
53
  Emitter.emits(*defined_events)
39
- Emitter.emits.should eq defined_events
54
+ Emitter.emits.should eq [:listener_added, defined_events].flatten
40
55
  end
41
56
 
42
57
  describe '.enable_strict!' do
@@ -97,51 +112,32 @@ describe Eventually do
97
112
  Emitter.emits(:jigger, arity: 5)
98
113
  Emitter.arity_for_event(:jigger).should eq 5
99
114
  Emitter.emits(:pingpong)
100
- Emitter.arity_for_event(:pingpong).should eq -1
115
+ Emitter.arity_for_event(:pingpong).should eq -2
101
116
  Emitter.arity_for_event(:nonevent).should eq nil
102
117
  end
103
118
  end
104
119
  end
105
120
  end
106
121
 
107
- it 'allows event registration with lambda' do
108
- emitter.on(:start, lambda{ puts 'hi' })
109
- end
110
-
111
- it 'allows event registration with proc' do
112
- emitter.on(:start, proc{ puts 'hi' })
113
- end
114
-
115
- it 'allows event registration with block' do
116
- emitter.on(:start) do
117
- puts 'hi'
118
- end
119
- end
120
-
121
- it 'allows event registration with detached method' do
122
- def event_handler; end
123
- emitter.on(:start, method(:event_handler))
124
- end
125
-
126
122
  it 'allows multiple registrations for a given event' do
127
123
  emitter.on(:start) { puts 'hello' }
128
124
  emitter.on(:start) { puts 'world' }
129
125
  end
130
126
 
131
- describe '.can_emit_or_register?' do
127
+ describe '.emittable?' do
132
128
  context 'when strict mode enabled' do
133
129
  before { Emitter.enable_strict! }
134
130
  context 'when given event is registered' do
135
131
  it 'returns true' do
136
132
  Emitter.emits(:known)
137
133
  Emitter.emits?(:known).should be_true
138
- Emitter.can_emit_or_register?(:known).should be_true
134
+ Emitter.emittable?(:known).should be_true
139
135
  end
140
136
  end
141
137
  context 'when given event is not registered' do
142
138
  it 'returns false' do
143
139
  Emitter.emits?(:unknown).should be_false
144
- Emitter.can_emit_or_register?(:unknown).should be_false
140
+ Emitter.emittable?(:unknown).should be_false
145
141
  end
146
142
  end
147
143
  end
@@ -152,20 +148,64 @@ describe Eventually do
152
148
  it 'returns true' do
153
149
  Emitter.emits(:known)
154
150
  Emitter.emits?(:known).should be_true
155
- Emitter.can_emit_or_register?(:known).should be_true
151
+ Emitter.emittable?(:known).should be_true
156
152
  end
157
153
  end
158
154
  context 'when given event is not registered' do
159
155
  it 'returns true' do
160
156
  Emitter.emits?(:unknown).should be_false
161
- Emitter.can_emit_or_register?(:unknown).should be_true
157
+ Emitter.emittable?(:unknown).should be_true
162
158
  end
163
159
  end
164
160
  end
165
161
  end
166
162
  end
167
163
 
168
- context 'when emitting events' do
164
+ describe '#on' do
165
+ it 'raises an error when a given callback is invalid' do
166
+ expect { emitter.on(:start, nil, &nil) }.should raise_error(/Cannot register callable/)
167
+ expect { emitter.on(:start, 10_000) }.should raise_error(/Cannot register callable/)
168
+ expect { emitter.on(:start, "callback") }.should raise_error(/Cannot register callable/)
169
+ end
170
+
171
+ context 'when arity validation is enabled' do
172
+ before { Emitter.emits(:hello_world, arity: 2) }
173
+ it 'accepts a callback with matching arity' do
174
+ expect {
175
+ emitter.on(:hello_world) do |param1, param2|
176
+ # callback will not be invoked
177
+ end
178
+ }.should_not raise_error
179
+ end
180
+
181
+ it 'rejects a callback if the given arity is not exact' do
182
+ expect {
183
+ emitter.on(:hello_world) do |param1, param2, param3|
184
+ # callback will not be invoked
185
+ end
186
+ }.should raise_error(/Arity validation failed for event :hello_world \(expected 2, received 3\)/)
187
+ end
188
+ end
189
+
190
+ context 'when detecting potential memory leaks' do
191
+ it 'writes a warning to stdout when > 10 listeners are added' do
192
+ $stdout.should_receive(:puts).once.with('Warning: Emitter has more than 10 registered listeners.')
193
+ (emitter.class.max_listeners+1).times do
194
+ emitter.on(:some_event, lambda{})
195
+ end
196
+ end
197
+
198
+ it 'will not print a warning if max_listeners is set to 0' do
199
+ emitter.class.max_listeners = 0
200
+ $stdout.should_not_receive(:puts).with('Warning: Emitter has more than 10 registered listeners.')
201
+ 1000.times do
202
+ emitter.on(:some_event, lambda{})
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ describe '#emit' do
169
209
  let(:emitter) { Emitter.new }
170
210
  let(:watcher) { Watcher.new }
171
211
 
@@ -186,7 +226,6 @@ describe Eventually do
186
226
  def proc_callback
187
227
  proc{|payload| @value += payload }
188
228
  end
189
-
190
229
  def update_method(payload)
191
230
  @value += payload
192
231
  end
@@ -216,14 +255,18 @@ describe Eventually do
216
255
  it_behaves_like 'emitting an event', :method
217
256
  it_behaves_like 'emitting an event', :proc
218
257
 
219
- it 'emits nothing when no event callbacks are given' do
220
- expect { emitter.__send__(:emit, :hullabaloo) }.should_not raise_error
258
+ it 'emits an event on the emitter when a new listener is added' do
259
+ callback_to_add = lambda{ puts 'hi' }
260
+ listener_id = nil
261
+ emitter.on(:listener_added) do |listener|
262
+ listener_id = listener.callable.object_id
263
+ end
264
+ emitter.on(:some_event, callback_to_add)
265
+ listener_id.should eq callback_to_add.object_id
221
266
  end
222
267
 
223
- it 'raises an error when a given callback is invalid' do
224
- expect { emitter.on(:start, nil, &nil) }.should raise_error(/Cannot register callback/)
225
- expect { emitter.on(:start, 10_000) }.should raise_error(/Cannot register callback/)
226
- expect { emitter.on(:start, "callback") }.should raise_error(/Cannot register callback/)
268
+ it 'emits nothing when no event callbacks are given' do
269
+ expect { emitter.__send__(:emit, :hullabaloo) }.should_not raise_error
227
270
  end
228
271
 
229
272
  it 'invokes registered callbacks in a FIFO manner' do
@@ -244,33 +287,18 @@ describe Eventually do
244
287
  end
245
288
 
246
289
  context 'when arity validation is enabled' do
247
- before { Emitter.emits(:hello_world, arity: 2) }
248
- it 'accepts a callback with matching arity' do
249
- expect {
250
- emitter.on(:hello_world) do |param1, param2|
251
- #not invoked
252
- end
253
- }.should_not raise_error
254
- end
255
-
256
- it 'rejects a callback if the given arity is not exact' do
257
- expect {
258
- emitter.on(:hello_world) do |param1, param2, param3|
259
- #not invoked
260
- end
261
- }.should raise_error(/Invalid callback arity for event :hello_world \(expected 2, received 3\)/)
262
- end
263
-
264
- it 'accepts emitting an event when arity is valid' do
290
+ it 'emits the event when arity is valid' do
291
+ emitter.class.emits(:hello_world, arity: 2)
265
292
  expect {
266
293
  emitter.__send__(:emit, :hello_world, "hello", "world")
267
294
  }.should_not raise_error
268
295
  end
269
296
 
270
297
  it 'rejects emitting an event when the arity is not exact' do
298
+ emitter.class.emits(:hello_world, arity: 2)
271
299
  expect {
272
300
  emitter.__send__(:emit, :hello_world, "hello")
273
- }.should raise_error(/Invalid emit arity for event :hello_world \(expected 2, received 1\)/)
301
+ }.should raise_error(/Arity validation failed for event :hello_world \(expected 2, received 1\)/)
274
302
  end
275
303
  end
276
304
 
@@ -286,40 +314,76 @@ describe Eventually do
286
314
  end
287
315
  end
288
316
  end
289
-
290
- describe '#valid_event_arity?' do
291
- context 'when arity is validated for event' do
292
- context 'and the arity matches' do
293
- it 'doesn\'t raise an error' do
294
- Emitter.emits(:jump, arity: 2)
295
- expect {
296
- emitter.on(:jump) do |param1, param2|
297
- puts 'hi'
298
- end
299
- }.should_not raise_error
300
- end
301
- end
302
- context 'and the arity does not match' do
303
- it 'raises an error' do
304
- Emitter.emits(:jump, arity: 2)
305
- expect {
306
- emitter.on(:jump) do |param1|
307
- puts 'hi'
308
- end
309
- }.should raise_error(/expected 2, received 1/)
310
- end
311
- end
317
+ end
318
+
319
+ describe '#once' do
320
+ it 'registers to an event for one time only, then releases the registration' do
321
+ value = 1
322
+ emitter.once(:start) do
323
+ value += 1
312
324
  end
313
- context 'when arity is not for event' do
314
- it 'doesn\'t raise an error' do
315
- Emitter.emits?(:jump).should be_false
316
- expect {
317
- emitter.on(:jump) do |param1, param2|
318
- puts 'hi'
319
- end
320
- }.should_not raise_error
321
- end
325
+ listener_count = emitter.num_listeners
326
+
327
+ emitter.__send__(:emit, :start)
328
+ emitter.__send__(:emit, :start)
329
+ value.should eq 2
330
+ emitter.num_listeners.should eq(listener_count - 1)
331
+ end
332
+ end
333
+
334
+ describe '#remove_listener' do
335
+ it 'removes a given event callback' do
336
+ value = 1
337
+ cbk = lambda{ value += 1 }
338
+ emitter.on(:some_event, cbk)
339
+ emitter.__send__(:emit, :some_event)
340
+ value.should eq 2
341
+ emitter.remove_listener(:some_event, cbk)
342
+ emitter.__send__(:emit, :some_event)
343
+ value.should eq 2
344
+ end
345
+ end
346
+
347
+ describe '#remove_all_listeners' do
348
+ it 'removes all listeners for the given event' do
349
+ value = 1
350
+ emitter.on(:some_event) do
351
+ value += 1
352
+ end
353
+ emitter.on(:some_event) do
354
+ value += 5
355
+ end
356
+ emitter.on(:some_event) do
357
+ value += 10
322
358
  end
359
+ emitter.__send__(:emit, :some_event)
360
+ value.should eq 17
361
+
362
+ emitter.remove_all_listeners(:some_event)
363
+ emitter.__send__(:emit, :some_event)
364
+ value.should eq 17
323
365
  end
324
366
  end
367
+
368
+ describe '#listeners' do
369
+ it 'returns a list of listener callbacks for the given event' do
370
+ cb1 = lambda{}
371
+ cb2 = lambda{}
372
+ cb3 = lambda{}
373
+ emitter.on(:some_event, cb1)
374
+ emitter.on(:some_event, cb2)
375
+ emitter.on(:some_event, cb3)
376
+ emitter.listeners(:some_event).should eq [cb1, cb2, cb3]
377
+ end
378
+ end
379
+
380
+ describe '#num_listeners' do
381
+ it 'counts all listeners across all events' do
382
+ emitter.on(:event1, lambda{})
383
+ emitter.on(:event2, lambda{})
384
+ emitter.on(:event3, lambda{})
385
+ emitter.num_listeners.should eq 3
386
+ end
387
+ end
388
+
325
389
  end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ require 'eventually/validation/arity'
3
+
4
+ unless defined?(ArityEmitter)
5
+ class ArityEmitter
6
+ include Eventually
7
+ emits :started, arity: 1
8
+ emits :stopped
9
+ end
10
+ end
11
+
12
+ describe Eventually::Validation::Arity do
13
+ let(:emitter) { ArityEmitter.new }
14
+
15
+ describe '#valid?' do
16
+ context 'when arity defined for event' do
17
+ context 'and arity does not match' do
18
+ subject { Eventually::Validation::Arity.new(:started, emitter, 2) }
19
+ it 'is invalid' do
20
+ subject.valid?.should eq false
21
+ subject.expected.should eq 1
22
+ subject.received.should eq 2
23
+ end
24
+ end
25
+ context 'and arity matches' do
26
+ subject { Eventually::Validation::Arity.new(:started, emitter, 1) }
27
+ it 'is valid' do
28
+ subject.valid?.should eq true
29
+ subject.expected.should eq 1
30
+ subject.received.should eq 1
31
+ end
32
+ end
33
+ end
34
+ context 'when event is defined' do
35
+ context 'and arity is not defined' do
36
+ subject { Eventually::Validation::Arity.new(:stopped, emitter, 3) }
37
+ it 'is valid' do
38
+ subject.valid?.should eq true
39
+ subject.expected.should eq -2
40
+ subject.received.should eq 3
41
+ end
42
+ end
43
+ end
44
+ context 'when event is not defined' do
45
+ context 'and arity is not defined' do
46
+ subject { Eventually::Validation::Arity.new(:something_else, emitter, 3) }
47
+ it 'is valid' do
48
+ subject.valid?.should eq true
49
+ subject.expected.should eq nil
50
+ subject.received.should eq 3
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ describe '#raise_unless_valid!' do
57
+ subject { Eventually::Validation::Arity.new(:an_event, emitter, 0)}
58
+
59
+ context 'when not valid' do
60
+ it 'does not raise an error' do
61
+ subject.stub(:valid?).and_return(true)
62
+ expect { subject.raise_unless_valid! }.should_not raise_error
63
+ end
64
+ end
65
+
66
+ context 'when not valid' do
67
+ it 'raises an error' do
68
+ subject.stub(:valid?).and_return(false)
69
+ expect { subject.raise_unless_valid! }.should raise_error
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require 'eventually/validation/max_listeners'
3
+
4
+ unless defined?(MaxListenerEmitter)
5
+ class MaxListenerEmitter
6
+ include Eventually
7
+ end
8
+ end
9
+
10
+ describe Eventually::Validation::MaxListeners do
11
+ let(:emitter) { MaxListenerEmitter.new }
12
+ subject { Eventually::Validation::MaxListeners.new(emitter) }
13
+
14
+ describe '#valid?' do
15
+ context 'when max_listeners is non-zero positive integer' do
16
+ context 'when num_listeners is less than or equal to max_listeners' do
17
+ it 'is valid' do
18
+ emitter.class.max_listeners = 5
19
+ 5.times{|i| emitter.on("event"+i.to_s, lambda{}) }
20
+ subject.valid?.should eq true
21
+ end
22
+ end
23
+ context 'when num_listeners is greater than max_listeners' do
24
+ it 'is not valid' do
25
+ emitter.class.max_listeners = 3
26
+ 5.times{|i| emitter.on("event"+i.to_s, lambda{}) }
27
+ subject.valid?.should eq false
28
+ end
29
+ end
30
+ end
31
+ context 'when max_listeners is zero' do
32
+ it 'is valid' do
33
+ emitter.class.max_listeners = 0
34
+ 1000.times{|i| emitter.on("event"+i.to_s, lambda{}) }
35
+ subject.valid?.should eq true
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#warn_unless_valid!' do
41
+ context 'when not valid' do
42
+ it 'does not raise an error' do
43
+ subject.stub(:valid?).and_return(true)
44
+ $stdout.should_not_receive(:puts)
45
+ subject.warn_unless_valid!
46
+ end
47
+ end
48
+
49
+ context 'when not valid' do
50
+ it 'raises an error' do
51
+ subject.stub(:valid?).and_return(false)
52
+ $stdout.should_receive(:puts).with(/Warning: MaxListenerEmitter has more than \d+ registered listeners\./)
53
+ subject.warn_unless_valid!
54
+ end
55
+ end
56
+ end
57
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  $LOAD_PATH << File.expand_path('../lib', File.dirname(__FILE__))
2
+ require 'simplecov'
3
+ SimpleCov.start
2
4
  require 'eventually'
3
- require 'rspec'
5
+ require 'rspec'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eventually
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
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: 2011-12-20 00:00:00.000000000Z
12
+ date: 2011-12-21 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
16
- requirement: &2153560520 !ruby/object:Gem::Requirement
16
+ requirement: &2161573120 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,29 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2153560520
24
+ version_requirements: *2161573120
25
+ - !ruby/object:Gem::Dependency
26
+ name: yard
27
+ requirement: &2161572700 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2161572700
36
+ - !ruby/object:Gem::Dependency
37
+ name: simplecov
38
+ requirement: &2152246440 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2152246440
25
47
  description: Eventually is an event library built to loosely mirror the NodeJS EventEmitter
26
48
  API.
27
49
  email:
@@ -40,8 +62,16 @@ files:
40
62
  - examples/basic.rb
41
63
  - examples/strict.rb
42
64
  - lib/eventually.rb
65
+ - lib/eventually/callable.rb
66
+ - lib/eventually/event.rb
67
+ - lib/eventually/validation/arity.rb
68
+ - lib/eventually/validation/max_listeners.rb
43
69
  - lib/eventually/version.rb
70
+ - spec/lib/callable_spec.rb
71
+ - spec/lib/event_spec.rb
44
72
  - spec/lib/eventually_spec.rb
73
+ - spec/lib/validation/arity_spec.rb
74
+ - spec/lib/validation/max_listeners_spec.rb
45
75
  - spec/spec_helper.rb
46
76
  homepage: http://www.rand9.com
47
77
  licenses: []
@@ -69,5 +99,10 @@ specification_version: 3
69
99
  summary: Eventually is an event library built to loosely mirror the NodeJS EventEmitter
70
100
  API.
71
101
  test_files:
102
+ - spec/lib/callable_spec.rb
103
+ - spec/lib/event_spec.rb
72
104
  - spec/lib/eventually_spec.rb
105
+ - spec/lib/validation/arity_spec.rb
106
+ - spec/lib/validation/max_listeners_spec.rb
73
107
  - spec/spec_helper.rb
108
+ has_rdoc: