brainguy 0.0.1
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.
- 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
|