brainguy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.yardopts +4 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.erb +345 -0
  7. data/README.markdown +579 -0
  8. data/Rakefile +21 -0
  9. data/brainguy.gemspec +28 -0
  10. data/examples/include_manifestly_observable.rb +22 -0
  11. data/examples/include_observable.rb +18 -0
  12. data/examples/include_observer.rb +36 -0
  13. data/examples/manual_observable.rb +22 -0
  14. data/examples/open_observer.rb +31 -0
  15. data/examples/proc_observer.rb +10 -0
  16. data/examples/scoped_subscription.rb +39 -0
  17. data/examples/synopsis.rb +56 -0
  18. data/lib/brainguy.rb +34 -0
  19. data/lib/brainguy/basic_notifier.rb +19 -0
  20. data/lib/brainguy/emitter.rb +110 -0
  21. data/lib/brainguy/error_collecting_notifier.rb +26 -0
  22. data/lib/brainguy/error_handling_notifier.rb +63 -0
  23. data/lib/brainguy/event.rb +13 -0
  24. data/lib/brainguy/fluent_emitter.rb +30 -0
  25. data/lib/brainguy/full_subscription.rb +8 -0
  26. data/lib/brainguy/idempotent_emitter.rb +40 -0
  27. data/lib/brainguy/manifest_emitter.rb +78 -0
  28. data/lib/brainguy/manifestly_observable.rb +62 -0
  29. data/lib/brainguy/observable.rb +33 -0
  30. data/lib/brainguy/observer.rb +71 -0
  31. data/lib/brainguy/open_observer.rb +65 -0
  32. data/lib/brainguy/single_event_subscription.rb +31 -0
  33. data/lib/brainguy/subscription.rb +59 -0
  34. data/lib/brainguy/subscription_scope.rb +62 -0
  35. data/lib/brainguy/version.rb +4 -0
  36. data/scripts/benchmark_listener_dispatch.rb +222 -0
  37. data/spec/brainguy/emitter_spec.rb +25 -0
  38. data/spec/brainguy/error_collecting_notifier_spec.rb +19 -0
  39. data/spec/brainguy/error_handling_notifier_spec.rb +63 -0
  40. data/spec/brainguy/manifest_emitter_spec.rb +68 -0
  41. data/spec/brainguy/manifestly_observable_spec.rb +43 -0
  42. data/spec/brainguy/observable_spec.rb +9 -0
  43. data/spec/brainguy/observer_spec.rb +72 -0
  44. data/spec/brainguy/open_observer_spec.rb +57 -0
  45. data/spec/brainguy/single_event_subscription_spec.rb +16 -0
  46. data/spec/brainguy/subscription_scope_spec.rb +72 -0
  47. data/spec/brainguy/subscription_spec.rb +46 -0
  48. data/spec/features/basics_spec.rb +153 -0
  49. data/spec/features/idempotent_events_spec.rb +69 -0
  50. data/spec/features/method_scoped_events_spec.rb +90 -0
  51. data/spec/support/shared_examples_for_eventful_modules.rb +36 -0
  52. 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,4 @@
1
+ module Brainguy
2
+ # The library version
3
+ VERSION = "0.0.1"
4
+ 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