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,68 @@
1
+ require "rspec"
2
+ require "brainguy/manifest_emitter"
3
+
4
+ module Brainguy
5
+ RSpec.describe ManifestEmitter do
6
+ it "starts with an empty set of known events" do
7
+ set = ManifestEmitter.new(instance_double(Emitter))
8
+ expect(set.known_types).to be_empty
9
+ end
10
+
11
+ it "warns when a handler is added for an unknown event" do
12
+ set = ManifestEmitter.new(
13
+ instance_double(Emitter, on: nil))
14
+ expect { set.on(:foo) {} }
15
+ .to output(/unknown event type 'foo'/i).to_stderr
16
+ end
17
+
18
+ it "warns when an unknown event is emitted" do
19
+ set = ManifestEmitter.new(
20
+ instance_double(Emitter, emit: nil))
21
+ expect { set.emit(:foo) {} }
22
+ .to output(/unknown event type 'foo'/i).to_stderr
23
+ end
24
+
25
+ it "does not warn when a handler is added for an known event" do
26
+ set = ManifestEmitter.new(
27
+ instance_double(Emitter, on: nil))
28
+ set.known_types << :foo
29
+ expect { set.on(:foo) {} }
30
+ .not_to output.to_stderr
31
+ end
32
+
33
+ it "does not warn when an known event is emitted" do
34
+ set = ManifestEmitter.new(
35
+ instance_double(Emitter, emit: nil))
36
+ set.known_types << :foo
37
+ expect { set.emit(:foo) {} }
38
+ .not_to output.to_stderr
39
+ end
40
+
41
+ context "configured to raise errors" do
42
+ it "fails when a handler is added for an unknown event" do
43
+ set = ManifestEmitter.new(
44
+ instance_double(Emitter, on: nil),
45
+ policy: :raise_error)
46
+ expect { set.on(:foo) {} }
47
+ .to raise_error(UnknownEvent, /unknown event type 'foo'/i)
48
+ end
49
+
50
+ it "fails when an unknown event is emitted" do
51
+ set = ManifestEmitter.new(
52
+ instance_double(Emitter, emit: nil),
53
+ policy: :raise_error)
54
+ expect { set.emit(:foo) {} }
55
+ .to raise_error(UnknownEvent, /unknown event type 'foo'/i)
56
+ end
57
+
58
+ it "reports exception from the send point" do
59
+ set = ManifestEmitter.new(
60
+ instance_double(Emitter, emit: nil),
61
+ policy: :raise_error)
62
+ error = set.emit(:foo) rescue $!; line = __LINE__
63
+ file = File.basename(__FILE__)
64
+ expect(error.backtrace[0]).to include("#{file}:#{line}")
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,43 @@
1
+ require "rspec"
2
+ require "brainguy/manifestly_observable"
3
+ require "support/shared_examples_for_eventful_modules"
4
+
5
+ module Brainguy
6
+ RSpec.describe ManifestlyObservable do
7
+ include_examples "an eventful module",
8
+ ManifestlyObservable.new(:heat, :drip, :done)
9
+
10
+ it "adds a ManifestEmitter to the class" do
11
+ klass = Class.new do
12
+ include ManifestlyObservable.new(:red, :green)
13
+ end
14
+ obj = klass.new
15
+ expect(obj.events).to be_a(ManifestEmitter)
16
+ end
17
+
18
+ it "adds the specified event names to the known list" do
19
+ klass = Class.new do
20
+ include ManifestlyObservable.new(:red, :green)
21
+ end
22
+ obj = klass.new
23
+ expect(obj.events.known_types).to eq([:red, :green])
24
+ end
25
+
26
+ it "inherits event names" do
27
+ parent = Class.new do
28
+ include ManifestlyObservable.new(:red, :green)
29
+ end
30
+ child = Class.new(parent) do
31
+ include ManifestlyObservable.new(:yellow)
32
+ end
33
+ obj = child.new
34
+ expect(obj.events.known_types).to eq([:red, :green, :yellow])
35
+ end
36
+
37
+ it "stringifies meaningfully" do
38
+ mod = ManifestlyObservable.new(:red, :green)
39
+ expect(mod.to_s).to eq("ManifestlyObservable(:red, :green)")
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ require "rspec"
2
+ require "brainguy/observable"
3
+ require "support/shared_examples_for_eventful_modules"
4
+
5
+ module Brainguy
6
+ RSpec.describe Observable do
7
+ include_examples "an eventful module", Observable
8
+ end
9
+ end
@@ -0,0 +1,72 @@
1
+ require "rspec"
2
+ require "brainguy/observer"
3
+
4
+ module Brainguy
5
+ RSpec.describe Observer do
6
+ class AccountListener
7
+ def on_open(event)
8
+ end
9
+
10
+ include Observer
11
+
12
+ def on_deposit(event)
13
+ end
14
+
15
+ def on_withdrawal(event)
16
+ end
17
+ end
18
+
19
+ it "routes events to their appropriate handler methods" do
20
+ listener = AccountListener.new
21
+ allow(listener).to receive(:on_deposit)
22
+ allow(listener).to receive(:on_withdrawal)
23
+
24
+ listener.call(e1 = Event[:deposit])
25
+ expect(listener).to have_received(:on_deposit).with(e1)
26
+
27
+ listener.call(e2 = Event[:withdrawal])
28
+ expect(listener).to have_received(:on_withdrawal).with(e2)
29
+ end
30
+
31
+ it "picks up handler methods defined before module inclusion" do
32
+ klass = Class.new do
33
+ def on_open(event)
34
+
35
+ end
36
+
37
+ include Observer
38
+ end
39
+ listener = klass.new
40
+ allow(listener).to receive(:on_open)
41
+ listener.call(e2 = Event[:open])
42
+ expect(listener).to have_received(:on_open).with(e2)
43
+ end
44
+
45
+ it "ignores events which lack handler methods" do
46
+ listener = AccountListener.new
47
+ expect { listener.call(Event[:not_defined]) }.to_not raise_error
48
+ end
49
+
50
+ it "doesn't blow up on a handler-less class" do
51
+ expect do
52
+ Class.new do
53
+ include Observer
54
+ end
55
+ end.to_not raise_error
56
+ end
57
+
58
+ it "passes unknown events to a catch-all method" do
59
+ events = []
60
+ klass = Class.new do
61
+ include Observer
62
+ define_method :event_handler_missing do |event|
63
+ events << event
64
+ end
65
+ end
66
+ listener = klass.new
67
+ listener.call(unknown_event = Event[:unknown_event])
68
+ expect(events).to eq([unknown_event])
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,57 @@
1
+ require "rspec"
2
+ require "brainguy/open_observer"
3
+
4
+ module Brainguy
5
+ RSpec.describe OpenObserver do
6
+ it "can be constructed from a hash" do
7
+ probe = spy("probe")
8
+ ol = OpenObserver.new(foo: ->(e) { probe.handle_foo(e) },
9
+ bar: ->(e) { probe.handle_bar(e) })
10
+ source = double("source")
11
+ ol.call(e1 = Event[:foo])
12
+ ol.call(e2 = Event[:bar])
13
+ expect(probe).to have_received(:handle_foo).with(e1)
14
+ expect(probe).to have_received(:handle_bar).with(e2)
15
+ end
16
+
17
+ it "can be constructed using on_* methods" do
18
+ probe = spy("probe")
19
+ ol = OpenObserver.new
20
+ ol.on_foo do |e|
21
+ probe.handle_foo(e)
22
+ end
23
+ .on_bar do |e|
24
+ probe.handle_bar(e)
25
+ end
26
+ source = double("source")
27
+ ol.call(e1 = Event[:foo])
28
+ ol.call(e2 = Event[:bar])
29
+ expect(probe).to have_received(:handle_foo).with(e1)
30
+ expect(probe).to have_received(:handle_bar).with(e2)
31
+ end
32
+
33
+ it "yields self on init" do
34
+ probe = spy("probe")
35
+ ol = OpenObserver.new do |l|
36
+ l.on_foo do |e|
37
+ probe.handle_foo(e)
38
+ end
39
+ l.on_bar do |e|
40
+ probe.handle_bar(e)
41
+ end
42
+ end
43
+ source = double("source")
44
+ ol.call(e1 = Event[:foo])
45
+ ol.call(e2 = Event[:bar])
46
+ expect(probe).to have_received(:handle_foo).with(e1)
47
+ expect(probe).to have_received(:handle_bar).with(e2)
48
+ end
49
+
50
+ it "has correct #respond_to? behavior" do
51
+ ol = OpenObserver.new
52
+ expect(ol).to respond_to(:on_blub)
53
+ expect(ol).to_not respond_to(:blub)
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,16 @@
1
+ require "rspec"
2
+ require "brainguy/single_event_subscription"
3
+
4
+ module Brainguy
5
+ RSpec.describe SingleEventSubscription do
6
+ it "is not equal to another subscription with a different event" do
7
+ owner = double("owner")
8
+ listener = double("listener")
9
+ sub1 = SingleEventSubscription.new(owner, listener, double)
10
+ sub2 = SingleEventSubscription.new(owner, listener, double)
11
+ expect(sub1).not_to eq(sub2)
12
+ expect(sub1.hash).not_to eq(sub2.hash)
13
+ expect(sub1).not_to eql(sub2)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,72 @@
1
+ require "rspec"
2
+ require "brainguy/subscription_scope"
3
+
4
+ module Brainguy
5
+ RSpec.describe SubscriptionScope do
6
+ it "proxies subscriptions to an underlying subscription set" do
7
+ scope = SubscriptionScope.new(set = instance_double(Emitter))
8
+ allow(set).to receive(:on).and_return(sub1 = double("sub1"))
9
+ allow(set).to receive(:attach).and_return(sub2 = double("sub2"))
10
+ expect(scope.on(:foo)).to be(sub1)
11
+ expect(scope.attach(arg2 = double)).to be(sub2)
12
+ expect(set).to have_received(:on).with(:foo)
13
+ expect(set).to have_received(:attach).with(arg2)
14
+ end
15
+
16
+ it "removes subscriptions on close" do
17
+ scope = SubscriptionScope.new(set = instance_double(Emitter))
18
+ allow(set).to receive(:on)
19
+ .and_return(sub1 = instance_spy(Subscription))
20
+ allow(set).to receive(:attach)
21
+ .and_return(sub2 = instance_spy(Subscription))
22
+ scope.on(:foo)
23
+ scope.attach(double)
24
+ scope.close
25
+ expect(sub1).to have_received(:cancel)
26
+ expect(sub2).to have_received(:cancel)
27
+ end
28
+
29
+ it "removes subscriptions only once" do
30
+ scope = SubscriptionScope.new(set = instance_double(Emitter))
31
+ allow(set).to receive(:on)
32
+ .and_return(sub1 = instance_spy(Subscription))
33
+ scope.on(:foo)
34
+ scope.close
35
+ scope.close
36
+ expect(sub1).to have_received(:cancel).once
37
+ end
38
+
39
+ it "supports a block form" do
40
+ set = instance_double(Emitter)
41
+ allow(set).to receive(:on)
42
+ .and_return(sub1 = instance_spy(Subscription))
43
+ allow(set).to receive(:attach)
44
+ .and_return(sub2 = instance_spy(Subscription))
45
+
46
+ SubscriptionScope.open(set) do |scope|
47
+ scope.on(:foo)
48
+ scope.attach(double)
49
+ end
50
+
51
+ expect(sub1).to have_received(:cancel)
52
+ expect(sub2).to have_received(:cancel)
53
+ end
54
+
55
+ it "ensures subscriptions are cancelled despite errors in block" do
56
+ set = instance_double(Emitter)
57
+ allow(set).to receive(:on)
58
+ .and_return(sub1 = instance_spy(Subscription))
59
+ allow(set).to receive(:attach)
60
+ .and_return(sub2 = instance_spy(Subscription))
61
+
62
+ SubscriptionScope.open(set) do |scope|
63
+ scope.on(:foo)
64
+ scope.attach(double)
65
+ fail "Whoopsie"
66
+ end rescue nil
67
+
68
+ expect(sub1).to have_received(:cancel)
69
+ expect(sub2).to have_received(:cancel)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,46 @@
1
+ require "rspec"
2
+ require "brainguy/subscription"
3
+
4
+ module Brainguy
5
+ RSpec.describe Subscription do
6
+
7
+ it "is equal to another subscription with the same owner and listener" do
8
+ listener = double("listener")
9
+ owner = double("owner")
10
+ sub1 = Subscription.new(owner, listener)
11
+ sub2 = Subscription.new(owner, listener)
12
+ expect(sub1).to eq(sub2)
13
+ expect(sub1.hash).to eq(sub2.hash)
14
+ expect(sub1).to eql(sub2)
15
+
16
+ set = Set.new([sub1])
17
+ expect(set.add?(sub2)).to be_nil
18
+ expect(set.size).to eq(1)
19
+ set.delete(sub2)
20
+ expect(set.size).to eq(0)
21
+ end
22
+
23
+ it "is not equal to another subscription with a different owner" do
24
+ listener = double("listener")
25
+ sub1 = Subscription.new(double, listener)
26
+ sub2 = Subscription.new(double, listener)
27
+ expect(sub1).not_to eq(sub2)
28
+ expect(sub1.hash).not_to eq(sub2.hash)
29
+ expect(sub1).not_to eql(sub2)
30
+ end
31
+
32
+ it "is not equal to another subscription with a different listener" do
33
+ owner = double("owner")
34
+ sub1 = Subscription.new(owner, double)
35
+ sub2 = Subscription.new(owner, double)
36
+ expect(sub1).not_to eq(sub2)
37
+ expect(sub1.hash).not_to eq(sub2.hash)
38
+ expect(sub1).not_to eql(sub2)
39
+ end
40
+
41
+ it "is frozen" do
42
+ s = Subscription.new(double, double)
43
+ expect(s).to be_frozen
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,153 @@
1
+ require "rspec"
2
+ require "brainguy"
3
+
4
+ module Brainguy
5
+ class AuctionBid
6
+ attr_reader :events
7
+
8
+ def initialize
9
+ @events = Brainguy::Emitter.new(self)
10
+ end
11
+
12
+ def reject_bid
13
+ events.emit(:rejected)
14
+ end
15
+
16
+ def make_counter_offer
17
+ events.emit(:rejected, counter_amount: 101, valid_for_minutes: 60)
18
+ end
19
+
20
+ def accept_bid
21
+ events.emit(:accepted)
22
+ end
23
+ end
24
+
25
+ RSpec.describe Brainguy do
26
+ it "enables objects to publish simple events" do
27
+ mike = spy("mike")
28
+ bid = AuctionBid.new
29
+ bid.events.on(:rejected) do
30
+ mike.handle_rejection
31
+ end
32
+ bid.reject_bid
33
+ expect(mike).to have_received(:handle_rejection)
34
+ end
35
+
36
+ it "allows multiple listeners to attach" do
37
+ mike = spy("mike")
38
+ crow = spy("crow")
39
+ bid = AuctionBid.new
40
+ bid.events.on(:rejected) do
41
+ mike.handle_rejection
42
+ end
43
+ bid.events.on(:rejected) do
44
+ crow.handle_rejection
45
+ end
46
+ bid.reject_bid
47
+ expect(mike).to have_received(:handle_rejection)
48
+ expect(crow).to have_received(:handle_rejection)
49
+ end
50
+
51
+ it "allows multiple events to be emitted" do
52
+ crow = spy("crow")
53
+ bid = AuctionBid.new
54
+ bid.events.on(:rejected) do
55
+ crow.handle_rejection
56
+ end
57
+ bid.events.on(:accepted) do
58
+ crow.handle_acceptance
59
+ end
60
+ bid.reject_bid
61
+ expect(crow).to have_received(:handle_rejection)
62
+ expect(crow).to_not have_received(:handle_acceptance)
63
+ bid.accept_bid
64
+ expect(crow).to have_received(:handle_acceptance)
65
+ end
66
+
67
+ it "allows for zero subscribers" do
68
+ bid = AuctionBid.new
69
+ expect { bid.reject_bid }.to_not raise_error
70
+ end
71
+
72
+ it "passes extra event args on to the handler block" do
73
+ bid = AuctionBid.new
74
+ expect do |b|
75
+ bid.events.on(:rejected, &b)
76
+ bid.make_counter_offer
77
+ end.to yield_with_args({counter_amount: 101,
78
+ valid_for_minutes: 60})
79
+ end
80
+
81
+ it "allows a subscription to be removed" do
82
+ bid = AuctionBid.new
83
+ expect do |b|
84
+ subscription = bid.events.on(:rejected, &b)
85
+ subscription.cancel
86
+ bid.reject_bid
87
+ end.to_not yield_control
88
+ end
89
+
90
+ it "allows an object to listen to all events" do
91
+ bid = AuctionBid.new
92
+ listener = spy("listener")
93
+ bid.events.attach(listener)
94
+
95
+ bid.reject_bid
96
+ expect(listener).to have_received(:call).with(Event[:rejected, bid])
97
+
98
+ bid.accept_bid
99
+ expect(listener).to have_received(:call).with(Event[:accepted, bid])
100
+ end
101
+
102
+ it "allows a listener to be unsubscribed" do
103
+ bid = AuctionBid.new
104
+ listener = spy("listener")
105
+ subscription = bid.events.attach(listener)
106
+
107
+ bid.reject_bid
108
+ expect(listener).to have_received(:call).with(Event[:rejected, bid])
109
+
110
+ subscription.cancel
111
+
112
+ bid.accept_bid
113
+ expect(listener).not_to have_received(:call).with(Event[:accepted, bid])
114
+ end
115
+
116
+ it "allows a listener to be unsubscribed by identity" do
117
+ bid = AuctionBid.new
118
+ listener = spy("listener")
119
+ bid.events.attach(listener)
120
+
121
+ bid.reject_bid
122
+ expect(listener).to have_received(:call).with(Event[:rejected, bid])
123
+
124
+ bid.events.detach(listener)
125
+ expect(bid.events).to be_empty
126
+
127
+ bid.accept_bid
128
+ expect(listener).not_to have_received(:call).with(Event[:accepted, bid])
129
+ end
130
+
131
+ it "does not allow the same listener to be subscribed twice" do
132
+ bid = AuctionBid.new
133
+ listener = spy("listener")
134
+ bid.events.attach(listener)
135
+ bid.events.attach(listener)
136
+ bid.reject_bid
137
+ expect(listener).to have_received(:call).once
138
+ end
139
+
140
+ it "provides scoped subscriptions" do
141
+ bid = AuctionBid.new
142
+ probe = spy("probe")
143
+ bid.events.with_subscription_scope do |scope|
144
+ scope.on(:rejected) do
145
+ probe.handle_rejection
146
+ end
147
+ bid.reject_bid
148
+ end
149
+ bid.reject_bid
150
+ expect(probe).to have_received(:handle_rejection).once
151
+ end
152
+ end
153
+ end