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