brainguy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.erb +345 -0
- data/README.markdown +579 -0
- data/Rakefile +21 -0
- data/brainguy.gemspec +28 -0
- data/examples/include_manifestly_observable.rb +22 -0
- data/examples/include_observable.rb +18 -0
- data/examples/include_observer.rb +36 -0
- data/examples/manual_observable.rb +22 -0
- data/examples/open_observer.rb +31 -0
- data/examples/proc_observer.rb +10 -0
- data/examples/scoped_subscription.rb +39 -0
- data/examples/synopsis.rb +56 -0
- data/lib/brainguy.rb +34 -0
- data/lib/brainguy/basic_notifier.rb +19 -0
- data/lib/brainguy/emitter.rb +110 -0
- data/lib/brainguy/error_collecting_notifier.rb +26 -0
- data/lib/brainguy/error_handling_notifier.rb +63 -0
- data/lib/brainguy/event.rb +13 -0
- data/lib/brainguy/fluent_emitter.rb +30 -0
- data/lib/brainguy/full_subscription.rb +8 -0
- data/lib/brainguy/idempotent_emitter.rb +40 -0
- data/lib/brainguy/manifest_emitter.rb +78 -0
- data/lib/brainguy/manifestly_observable.rb +62 -0
- data/lib/brainguy/observable.rb +33 -0
- data/lib/brainguy/observer.rb +71 -0
- data/lib/brainguy/open_observer.rb +65 -0
- data/lib/brainguy/single_event_subscription.rb +31 -0
- data/lib/brainguy/subscription.rb +59 -0
- data/lib/brainguy/subscription_scope.rb +62 -0
- data/lib/brainguy/version.rb +4 -0
- data/scripts/benchmark_listener_dispatch.rb +222 -0
- data/spec/brainguy/emitter_spec.rb +25 -0
- data/spec/brainguy/error_collecting_notifier_spec.rb +19 -0
- data/spec/brainguy/error_handling_notifier_spec.rb +63 -0
- data/spec/brainguy/manifest_emitter_spec.rb +68 -0
- data/spec/brainguy/manifestly_observable_spec.rb +43 -0
- data/spec/brainguy/observable_spec.rb +9 -0
- data/spec/brainguy/observer_spec.rb +72 -0
- data/spec/brainguy/open_observer_spec.rb +57 -0
- data/spec/brainguy/single_event_subscription_spec.rb +16 -0
- data/spec/brainguy/subscription_scope_spec.rb +72 -0
- data/spec/brainguy/subscription_spec.rb +46 -0
- data/spec/features/basics_spec.rb +153 -0
- data/spec/features/idempotent_events_spec.rb +69 -0
- data/spec/features/method_scoped_events_spec.rb +90 -0
- data/spec/support/shared_examples_for_eventful_modules.rb +36 -0
- metadata +196 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
require "brainguy/subscription"
|
2
|
+
|
3
|
+
module Brainguy
|
4
|
+
# A subscription to a single type (name) of event. The `listener` will only
|
5
|
+
# be notified if the event name matches `subscribed_event_name`.
|
6
|
+
#
|
7
|
+
# See {Emitter#on} for where this class is used.
|
8
|
+
class SingleEventSubscription < Subscription
|
9
|
+
# @param owner [Emitter] the owning {Emitter}
|
10
|
+
# @param listener [:call] some callable that should be called when the
|
11
|
+
# named event occurs
|
12
|
+
# @param subscribed_event_name [Symbol] the event to subscribe to
|
13
|
+
def initialize(owner, listener, subscribed_event_name)
|
14
|
+
@subscribed_event_name = subscribed_event_name
|
15
|
+
super(owner, listener)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Call listener if the event name is the one being subscribed to.
|
19
|
+
# @param event [Event] the event to (maybe) dispatch
|
20
|
+
def handle(event)
|
21
|
+
return unless event.name == @subscribed_event_name
|
22
|
+
@listener.call(*event.args)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def equality_components
|
28
|
+
super + [@subscribed_event_name]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Brainguy
|
2
|
+
# A value object that represents a single subscription to an event source.
|
3
|
+
#
|
4
|
+
# It ties together an event `source` to a `listener`. A listener is simply
|
5
|
+
# some call-able object.
|
6
|
+
class Subscription
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
# @!attribute [r] owner
|
10
|
+
# @return [Emitter] the owning {Emitter}
|
11
|
+
# @!attribute [r] listener
|
12
|
+
# @return [:call] a callable listener object
|
13
|
+
attr_reader :owner, :listener
|
14
|
+
|
15
|
+
# @param owner [Emitter] the owning {Emitter}
|
16
|
+
# @param listener [:call] a callable listener object
|
17
|
+
def initialize(owner, listener)
|
18
|
+
@owner = owner
|
19
|
+
@listener = listener
|
20
|
+
freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
# Dispatch `event` to the listener.
|
24
|
+
# @param event [Event] the event to dispatch
|
25
|
+
# @return whatever the listener returns
|
26
|
+
def handle(event)
|
27
|
+
@listener.call(event)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Cancel the subscription (remove it from the `owner`)
|
31
|
+
def cancel
|
32
|
+
@owner.delete(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Compare this to another subscription
|
36
|
+
def <=>(other)
|
37
|
+
return nil unless other.is_a?(Subscription)
|
38
|
+
equality_components <=> other.equality_components
|
39
|
+
end
|
40
|
+
|
41
|
+
# The hash value is based on the identity (but not the state) of `owner`
|
42
|
+
# and `listener`. Two `Subscription` objects with the same `owner` and
|
43
|
+
# `listener` are considered to be equivalent.
|
44
|
+
def hash
|
45
|
+
[self.class, *equality_components].hash
|
46
|
+
end
|
47
|
+
|
48
|
+
# @!method eql?(other)
|
49
|
+
# @param other [Subscription] the other subscription to compare to
|
50
|
+
# @return [Boolean]
|
51
|
+
alias_method :eql?, :==
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def equality_components
|
56
|
+
[owner.object_id, listener.object_id]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "brainguy/emitter"
|
2
|
+
require "delegate"
|
3
|
+
|
4
|
+
module Brainguy
|
5
|
+
# A scope for subscriptions with a limited lifetime.
|
6
|
+
#
|
7
|
+
# Sometimes it's useful to have a set of subscriptions with a limited
|
8
|
+
# lifetime. For instance, a set of observers which are only valid over the
|
9
|
+
# course of a single method call. This class wraps an existing
|
10
|
+
# {Emitter}, and exposes the same API. But when a client sends the
|
11
|
+
# `#close` message, all listeners subscribed through this object will be
|
12
|
+
# immediately unsubscribed.
|
13
|
+
class SubscriptionScope < DelegateClass(Emitter)
|
14
|
+
|
15
|
+
# Create a new scope and yield it to the block. Closes the scope
|
16
|
+
# (unsubscribing any listeners attached using the scope) at the end of
|
17
|
+
# block execution
|
18
|
+
# @param (see #initialize)
|
19
|
+
# @yield [SubscriptionScope] the subscription scope
|
20
|
+
def self.open(subscription_set)
|
21
|
+
scope = self.new(subscription_set)
|
22
|
+
yield scope
|
23
|
+
ensure
|
24
|
+
scope.close
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param subscription_set [Emitter] the subscription set to wrap
|
28
|
+
def initialize(subscription_set)
|
29
|
+
super(subscription_set)
|
30
|
+
@subscriptions = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# (see Emitter#on)
|
34
|
+
def on(*)
|
35
|
+
super.tap do |subscription|
|
36
|
+
@subscriptions << subscription
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# (see Emitter#attach)
|
41
|
+
def attach(*)
|
42
|
+
super.tap do |subscription|
|
43
|
+
@subscriptions << subscription
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Detach every listener that was attached via this scope.
|
48
|
+
# @return [void]
|
49
|
+
def close
|
50
|
+
@subscriptions.each(&:cancel)
|
51
|
+
@subscriptions.clear
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Emitter
|
56
|
+
# @yield [SubscriptionScope] a temporary subscription scope layered on top
|
57
|
+
# of this {Emitter}
|
58
|
+
def with_subscription_scope(&block)
|
59
|
+
SubscriptionScope.open(self, &block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require "benchmark/ips"
|
2
|
+
|
3
|
+
Event = Struct.new(:name, :source, :args)
|
4
|
+
|
5
|
+
class TestBed
|
6
|
+
attr_reader :event_log
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@event_log = []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class HardcodeTestbed < TestBed
|
14
|
+
def call(event)
|
15
|
+
case event.name
|
16
|
+
when :foo
|
17
|
+
handle_foo(event)
|
18
|
+
when :bar
|
19
|
+
handle_bar(event)
|
20
|
+
when :baz
|
21
|
+
handle_baz(event)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_foo(event)
|
26
|
+
event_log << event
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_bar(event)
|
30
|
+
event_log << event
|
31
|
+
end
|
32
|
+
|
33
|
+
def handle_baz(event)
|
34
|
+
event_log << event
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SendTestbed < TestBed
|
39
|
+
def call(event)
|
40
|
+
handler_name = "handle_#{event.name}"
|
41
|
+
__send__(handler_name, event) if respond_to?(handler_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_foo(event)
|
45
|
+
event_log << event
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_bar(event)
|
49
|
+
event_log << event
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_baz(event)
|
53
|
+
event_log << event
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class SendTableTestbed < TestBed
|
58
|
+
def self.method_added(method_name)
|
59
|
+
if method_name.to_s =~ /\Ahandle_(.+)\z/
|
60
|
+
handler_methods[$1.to_sym] = method_name.to_sym
|
61
|
+
end
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.handler_methods
|
66
|
+
@handler_methods ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def call(event)
|
70
|
+
if (handler_method = self.class.handler_methods[event.name])
|
71
|
+
__send__(handler_method, event)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_foo(event)
|
76
|
+
event_log << event
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_bar(event)
|
80
|
+
event_log << event
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_baz(event)
|
84
|
+
event_log << event
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class BindTableTestbed < TestBed
|
89
|
+
def self.method_added(method_name)
|
90
|
+
if method_name.to_s =~ /\Ahandle_(.+)\z/
|
91
|
+
handler_methods[$1.to_sym] = instance_method(method_name)
|
92
|
+
end
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.handler_methods
|
97
|
+
@handler_methods ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def call(event)
|
101
|
+
if (handler_method = self.class.handler_methods[event.name])
|
102
|
+
handler_method.bind(self).call(event)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def handle_foo(event)
|
107
|
+
event_log << event
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_bar(event)
|
111
|
+
event_log << event
|
112
|
+
end
|
113
|
+
|
114
|
+
def handle_baz(event)
|
115
|
+
event_log << event
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class CodeGenTestbed < TestBed
|
120
|
+
def self.method_added(method_name)
|
121
|
+
if method_name.to_s =~ /\Ahandle_(.+)\z/
|
122
|
+
handler_methods << $1
|
123
|
+
regenerate_dispatch_method
|
124
|
+
end
|
125
|
+
super
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.handler_methods
|
129
|
+
@handler_methods ||= []
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.regenerate_dispatch_method
|
133
|
+
dispatch_table = handler_methods.map { |event_name|
|
134
|
+
"when :#{event_name} then handle_#{event_name}(event)"
|
135
|
+
}.join("\n")
|
136
|
+
class_eval %{
|
137
|
+
def call(event)
|
138
|
+
case event.name
|
139
|
+
#{dispatch_table}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
def handle_foo(event)
|
146
|
+
event_log << event
|
147
|
+
end
|
148
|
+
|
149
|
+
def handle_bar(event)
|
150
|
+
event_log << event
|
151
|
+
end
|
152
|
+
|
153
|
+
def handle_baz(event)
|
154
|
+
event_log << event
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class IfCodeGenTestbed < TestBed
|
159
|
+
def self.method_added(method_name)
|
160
|
+
if method_name.to_s =~ /\Ahandle_(.+)\z/
|
161
|
+
handler_methods << $1
|
162
|
+
regenerate_dispatch_method
|
163
|
+
end
|
164
|
+
super
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.handler_methods
|
168
|
+
@handler_methods ||= []
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.regenerate_dispatch_method
|
172
|
+
dispatch_table = handler_methods.map { |event_name|
|
173
|
+
"event.name.equal?(:#{event_name}) then handle_#{event_name}(event)"
|
174
|
+
}.join("\nelsif ")
|
175
|
+
class_eval %{
|
176
|
+
def call(event)
|
177
|
+
if #{dispatch_table}
|
178
|
+
end
|
179
|
+
end
|
180
|
+
}
|
181
|
+
end
|
182
|
+
|
183
|
+
def handle_foo(event)
|
184
|
+
event_log << event
|
185
|
+
end
|
186
|
+
|
187
|
+
def handle_bar(event)
|
188
|
+
event_log << event
|
189
|
+
end
|
190
|
+
|
191
|
+
def handle_baz(event)
|
192
|
+
event_log << event
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def do_test(klass)
|
197
|
+
testbed = klass.new
|
198
|
+
testbed.call(e1 = Event[:foo])
|
199
|
+
testbed.call(e2 = Event[:bar])
|
200
|
+
testbed.call(e3 = Event[:baz])
|
201
|
+
testbed.call(Event[:buz])
|
202
|
+
unless testbed.event_log == [e1, e2, e3]
|
203
|
+
fail "#{klass}: #{testbed.event_log.inspect}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
classes = [
|
208
|
+
HardcodeTestbed,
|
209
|
+
SendTestbed,
|
210
|
+
SendTableTestbed,
|
211
|
+
BindTableTestbed,
|
212
|
+
CodeGenTestbed,
|
213
|
+
IfCodeGenTestbed,
|
214
|
+
]
|
215
|
+
width = classes.map(&:name).map(&:size).max
|
216
|
+
|
217
|
+
Benchmark.ips do |x|
|
218
|
+
classes.each do |klass|
|
219
|
+
x.report(klass.name) { do_test(klass) }
|
220
|
+
end
|
221
|
+
x.compare!
|
222
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "brainguy/emitter"
|
3
|
+
|
4
|
+
module Brainguy
|
5
|
+
RSpec.describe Emitter do
|
6
|
+
it "uses notifier result for return value of #emit" do
|
7
|
+
notifier = instance_double(BasicNotifier, result: "THE RESULT")
|
8
|
+
ss = Emitter.new(double("event source"),
|
9
|
+
notifier_maker: ->() { notifier })
|
10
|
+
expect(ss.emit(:foo)).to eq("THE RESULT")
|
11
|
+
end
|
12
|
+
|
13
|
+
it "can set up multiple handlers at once with a hash of lambdas" do
|
14
|
+
ss = Emitter.new(double)
|
15
|
+
probe = spy("probe")
|
16
|
+
result = ss.on(foo: proc { probe.handle_foo },
|
17
|
+
bar: proc { probe.handle_bar })
|
18
|
+
expect(result.listener).to be_a(OpenObserver)
|
19
|
+
ss.emit(:foo)
|
20
|
+
ss.emit(:bar)
|
21
|
+
expect(probe).to have_received(:handle_foo)
|
22
|
+
expect(probe).to have_received(:handle_bar)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "brainguy/error_collecting_notifier"
|
3
|
+
|
4
|
+
module Brainguy
|
5
|
+
RSpec.describe ErrorCollectingNotifier do
|
6
|
+
it "collects errors into a list" do
|
7
|
+
inner = instance_spy(BasicNotifier, "inner")
|
8
|
+
outer = ErrorCollectingNotifier.new(inner)
|
9
|
+
error1 = StandardError.new("Whoopsie")
|
10
|
+
error2 = StandardError.new("O NOES")
|
11
|
+
allow(inner).to receive(:notify).and_raise(error1)
|
12
|
+
outer.notify(sub1 = double("subscription1"), double)
|
13
|
+
allow(inner).to receive(:notify).and_raise(error2)
|
14
|
+
outer.notify(sub2 = double("subscription2"), double)
|
15
|
+
expect(outer.result)
|
16
|
+
.to eq({sub1 => error1, sub2 => error2})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "rspec"
|
2
|
+
require "brainguy/error_handling_notifier"
|
3
|
+
|
4
|
+
module Brainguy
|
5
|
+
RSpec.describe ErrorHandlingNotifier do
|
6
|
+
it "passes notifications through" do
|
7
|
+
inner = instance_spy(BasicNotifier, "inner")
|
8
|
+
outer = ErrorHandlingNotifier.new(inner)
|
9
|
+
outer.notify(double, double)
|
10
|
+
expect(inner).to have_received(:notify)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "enables exceptions to be handled by an arbitrary callable" do
|
14
|
+
handler = double("handler", :call => nil)
|
15
|
+
inner = instance_spy(BasicNotifier, "inner")
|
16
|
+
outer = ErrorHandlingNotifier.new(inner, handler)
|
17
|
+
error = StandardError.new("Whoopsie")
|
18
|
+
allow(inner).to receive(:notify).and_raise(error)
|
19
|
+
outer.notify(sub = double("subscription"), double)
|
20
|
+
expect(handler).to have_received(:call).with(sub, error)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "re-raises errors if no handler is supplied" do
|
24
|
+
inner = instance_spy(BasicNotifier, "inner")
|
25
|
+
outer = ErrorHandlingNotifier.new(inner)
|
26
|
+
error = StandardError.new("Whoopsie")
|
27
|
+
allow(inner).to receive(:notify).and_raise(error)
|
28
|
+
expect{outer.notify(double, double)}.to raise_error(error)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "provides a mnemonic for the default re-raise behavior" do
|
32
|
+
inner = instance_spy(BasicNotifier, "inner")
|
33
|
+
outer = ErrorHandlingNotifier.new(inner, :raise)
|
34
|
+
error = StandardError.new("Whoopsie")
|
35
|
+
allow(inner).to receive(:notify).and_raise(error)
|
36
|
+
expect{outer.notify(double, double)}.to raise_error(error)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "provides a mnemonic for suppressing errors" do
|
40
|
+
inner = instance_spy(BasicNotifier, "inner")
|
41
|
+
outer = ErrorHandlingNotifier.new(inner, :suppress)
|
42
|
+
error = StandardError.new("Whoopsie")
|
43
|
+
allow(inner).to receive(:notify).and_raise(error)
|
44
|
+
expect{outer.notify(double, double)}.to_not raise_error
|
45
|
+
end
|
46
|
+
|
47
|
+
it "provides a mnemonic for converting errors to warnings" do
|
48
|
+
inner = instance_spy(BasicNotifier, "inner")
|
49
|
+
outer = ErrorHandlingNotifier.new(inner, :warn)
|
50
|
+
error = StandardError.new("Whoopsie")
|
51
|
+
allow(inner).to receive(:notify).and_raise(error)
|
52
|
+
expect{outer.notify(double, double)}
|
53
|
+
.to output(/StandardError: Whoopsie/).to_stderr
|
54
|
+
end
|
55
|
+
|
56
|
+
it "has a result of nil" do
|
57
|
+
inner = instance_spy(BasicNotifier, "inner")
|
58
|
+
outer = ErrorHandlingNotifier.new(inner)
|
59
|
+
outer.notify(double, double)
|
60
|
+
expect(outer.result).to be_nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|