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