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
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "yard"
|
3
|
+
|
4
|
+
task :build => :readme
|
5
|
+
|
6
|
+
desc "Build the README"
|
7
|
+
task :readme => "README.markdown"
|
8
|
+
|
9
|
+
file "README.markdown" => "README.erb" do
|
10
|
+
puts "Generating README.markdown"
|
11
|
+
require "erb"
|
12
|
+
template = IO.read("README.erb")
|
13
|
+
IO.write("README.markdown", ERB.new(template).result)
|
14
|
+
end
|
15
|
+
|
16
|
+
YARD::Rake::YardocTask.new do |t|
|
17
|
+
# t.files = ['lib/**/*.rb', OTHER_PATHS] # optional
|
18
|
+
# t.options = ['--any', '--extra', '--opts'] # optional
|
19
|
+
# t.stats_options = ['--list-undoc'] # optional
|
20
|
+
end
|
21
|
+
task :yard => :readme
|
data/brainguy.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'brainguy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "brainguy"
|
8
|
+
spec.version = Brainguy::VERSION
|
9
|
+
spec.authors = ["Avdi Grimm"]
|
10
|
+
spec.email = ["avdi@avdi.org"]
|
11
|
+
spec.summary = %q{An Observer pattern library}
|
12
|
+
spec.description = %q{A somewhat fancy observer pattern library with
|
13
|
+
features like named events and scoped subscriptions.}
|
14
|
+
spec.homepage = "https://github.com/avdi/brainguy"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
25
|
+
spec.add_development_dependency "benchmark-ips"
|
26
|
+
spec.add_development_dependency "yard", "~> 0.8.7"
|
27
|
+
spec.add_development_dependency "seeing_is_believing", "~> 2.2"
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class Toaster
|
4
|
+
include Brainguy::ManifestlyObservable.new(:start, :pop)
|
5
|
+
|
6
|
+
def make_toast
|
7
|
+
emit(:start)
|
8
|
+
emit(:lop)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
toaster = Toaster.new
|
13
|
+
toaster.events.unknown_event_policy = :raise_error
|
14
|
+
toaster.on(:plop) do
|
15
|
+
puts "Toast is done!"
|
16
|
+
end
|
17
|
+
toaster.make_toast
|
18
|
+
|
19
|
+
# ~> Brainguy::UnknownEvent
|
20
|
+
# ~> #on received for unknown event type 'plop'
|
21
|
+
# ~>
|
22
|
+
# ~> xmptmp-in27856uxq.rb:14:in `<main>'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class Toaster
|
4
|
+
include Brainguy::Observable
|
5
|
+
|
6
|
+
def make_toast
|
7
|
+
emit(:start)
|
8
|
+
emit(:pop)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
toaster = Toaster.new
|
13
|
+
toaster.on(:pop) do
|
14
|
+
puts "Toast is done!"
|
15
|
+
end
|
16
|
+
toaster.make_toast
|
17
|
+
|
18
|
+
# >> Toast is done!
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class Poem
|
4
|
+
include Brainguy::Observable
|
5
|
+
def recite
|
6
|
+
emit(:title, "Jabberwocky")
|
7
|
+
emit(:line, "'twas brillig, and the slithy toves")
|
8
|
+
emit(:line, "Did gyre and gimbal in the wabe")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class HtmlFormatter
|
13
|
+
include Brainguy::Observer
|
14
|
+
|
15
|
+
attr_reader :result
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@result = ""
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_title(event)
|
22
|
+
@result << "<h1>#{event.args.first}</h1>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_line(event)
|
26
|
+
@result << "#{event.args.first}</br>"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
p = Poem.new
|
31
|
+
f = HtmlFormatter.new
|
32
|
+
p.events.attach(f)
|
33
|
+
p.recite
|
34
|
+
|
35
|
+
f.result
|
36
|
+
# => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class Toaster
|
4
|
+
attr_reader :events
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@events = Brainguy::Emitter.new(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def make_toast
|
11
|
+
events.emit(:start)
|
12
|
+
events.emit(:pop)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
toaster = Toaster.new
|
17
|
+
toaster.events.on(:pop) do
|
18
|
+
puts "Toanst is done!"
|
19
|
+
end
|
20
|
+
toaster.make_toast
|
21
|
+
|
22
|
+
# >> Toast is done!
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class VideoRender
|
4
|
+
include Brainguy::Observable
|
5
|
+
attr_reader :name
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def do_render
|
11
|
+
emit(:complete)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
v1 = VideoRender.new("foo.mp4")
|
16
|
+
v2 = VideoRender.new("bar.mp4")
|
17
|
+
|
18
|
+
observer = Brainguy::OpenObserver.new do |o|
|
19
|
+
o.on_complete do |event|
|
20
|
+
puts "Video #{event.source.name} is done rendering!"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
v1.events.attach(observer)
|
25
|
+
v2.events.attach(observer)
|
26
|
+
|
27
|
+
v1.do_render
|
28
|
+
v2.do_render
|
29
|
+
|
30
|
+
# >> Video foo.mp4 is done rendering!
|
31
|
+
# >> Video bar.mp4 is done rendering!
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class Poem
|
4
|
+
include Brainguy::Observable
|
5
|
+
def recite(&block)
|
6
|
+
with_subscription_scope(block) do
|
7
|
+
emit(:title, "Jabberwocky")
|
8
|
+
emit(:line, "'twas brillig, and the slithy toves")
|
9
|
+
emit(:line, "Did gyre and gimbal in the wabe")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class HtmlFormatter
|
15
|
+
include Brainguy::Observer
|
16
|
+
|
17
|
+
attr_reader :result
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@result = ""
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_title(event)
|
24
|
+
@result << "<h1>#{event.args.first}</h1>"
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_line(event)
|
28
|
+
@result << "#{event.args.first}</br>"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
p = Poem.new
|
33
|
+
f = HtmlFormatter.new
|
34
|
+
p.recite do |events|
|
35
|
+
events.attach(f)
|
36
|
+
end
|
37
|
+
|
38
|
+
f.result
|
39
|
+
# => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "brainguy"
|
2
|
+
|
3
|
+
class SatelliteOfLove
|
4
|
+
include Brainguy::Observable
|
5
|
+
|
6
|
+
def intro_song
|
7
|
+
emit(:robot_roll_call)
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_the_movie
|
11
|
+
emit(:movie_sign)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Crew
|
16
|
+
include Brainguy::Observer
|
17
|
+
end
|
18
|
+
|
19
|
+
class TomServo < Crew
|
20
|
+
def on_robot_roll_call(event)
|
21
|
+
puts "Tom: Check me out!"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class CrowTRobot < Crew
|
26
|
+
def on_robot_roll_call(event)
|
27
|
+
puts "Crow: I'm different!"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class MikeNelson < Crew
|
32
|
+
def on_movie_sign(event)
|
33
|
+
puts "Mike: Oh no we've got movie sign!"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
sol = SatelliteOfLove.new
|
38
|
+
# Attach specific event handlers without a listener object
|
39
|
+
sol.on(:robot_roll_call) do
|
40
|
+
puts "[Robot roll call!]"
|
41
|
+
end
|
42
|
+
sol.on(:movie_sign) do
|
43
|
+
puts "[Movie sign flashes]"
|
44
|
+
end
|
45
|
+
sol.events.attach TomServo.new
|
46
|
+
sol.events.attach CrowTRobot.new
|
47
|
+
sol.events.attach MikeNelson.new
|
48
|
+
|
49
|
+
sol.intro_song
|
50
|
+
sol.send_the_movie
|
51
|
+
|
52
|
+
# >> [Robot roll call!]
|
53
|
+
# >> Tom: Check me out!
|
54
|
+
# >> Crow: I'm different!
|
55
|
+
# >> [Movie sign flashes]
|
56
|
+
# >> Mike: Oh no we've got movie sign!
|
data/lib/brainguy.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "brainguy/version"
|
2
|
+
require "brainguy/event"
|
3
|
+
require "brainguy/emitter"
|
4
|
+
require "brainguy/idempotent_emitter"
|
5
|
+
require "brainguy/observable"
|
6
|
+
require "brainguy/manifestly_observable"
|
7
|
+
|
8
|
+
require "brainguy/subscription_scope"
|
9
|
+
require "brainguy/fluent_emitter"
|
10
|
+
require "brainguy/observer"
|
11
|
+
|
12
|
+
# Namespace for the `brainguy` gem. See {file:README.md} for usage instructions.
|
13
|
+
module Brainguy
|
14
|
+
# Execute passed block with a temporary subscription scope. See README for
|
15
|
+
# examples.
|
16
|
+
#
|
17
|
+
# @param source the object initiating the event
|
18
|
+
# @param listener_block [:call] an optional callable that should hook up
|
19
|
+
# listeners
|
20
|
+
# @param subscription_set [Emitter] an existing subscription set to
|
21
|
+
# layer on top of
|
22
|
+
def self.with_subscription_scope(
|
23
|
+
source,
|
24
|
+
listener_block = nil,
|
25
|
+
subscription_set = IdempotentEmitter.new(source))
|
26
|
+
subscription_set.with_subscription_scope do |scope|
|
27
|
+
listener_block.call(scope) if listener_block
|
28
|
+
yield scope
|
29
|
+
end
|
30
|
+
unless listener_block
|
31
|
+
FluentEmitter.new(subscription_set)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Brainguy
|
2
|
+
# A notifier encapsulates various strategies for notifying subscriptions of
|
3
|
+
# events. This is the most basic form of notifier. It just passes the event
|
4
|
+
# on with no extra logic.
|
5
|
+
class BasicNotifier
|
6
|
+
# Notify a subscription of an event
|
7
|
+
#
|
8
|
+
# @return (see Subscription#handle)
|
9
|
+
def notify(subscription, event)
|
10
|
+
subscription.handle(event)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Some notifiers have interesting results. This one just returns nil.
|
14
|
+
# @return nil
|
15
|
+
def result
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "delegate"
|
2
|
+
require "set"
|
3
|
+
require "brainguy/full_subscription"
|
4
|
+
require "brainguy/single_event_subscription"
|
5
|
+
require "brainguy/event"
|
6
|
+
require "brainguy/basic_notifier"
|
7
|
+
require "brainguy/open_observer"
|
8
|
+
|
9
|
+
module Brainguy
|
10
|
+
# This object keeps track of all the listeners (observers) subscribed to a
|
11
|
+
# particular event source object.
|
12
|
+
class Emitter < DelegateClass(Set)
|
13
|
+
DEFAULT_NOTIFIER = BasicNotifier.new
|
14
|
+
|
15
|
+
# Create a new {Emitter} that shares its inner dataset with an
|
16
|
+
# existing one. This exists so that it's possible to generate temporary
|
17
|
+
# copies of a {Emitter} with different, specialized semantics;
|
18
|
+
# for instance, an {IdempotentEmitter} that shares the same
|
19
|
+
# set of subscriptions as an existing {Emitter}.
|
20
|
+
# @param event_source [Object] the event-originating object
|
21
|
+
# @param subscription_set [Emitter] the existing set to share
|
22
|
+
# subscriptions with
|
23
|
+
# @return [Emitter]
|
24
|
+
def self.new_from_existing(event_source, subscription_set)
|
25
|
+
new(event_source, subscriptions: subscription_set.subscriptions)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param event_source [Object] the event-originating object
|
29
|
+
# @option options [Set<Subscription>] :subscriptions (Set.new) the
|
30
|
+
# underlying set of subscriptions
|
31
|
+
# @option options [:call] :notifier_maker a factory for notifiers.
|
32
|
+
def initialize(event_source = self, options = {})
|
33
|
+
super(options[:subscriptions] || Set.new)
|
34
|
+
@event_source = event_source
|
35
|
+
@notifier_maker = options.fetch(:notifier_maker) {
|
36
|
+
->() { DEFAULT_NOTIFIER }
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [Set<Subscription>] the underlying set of subscription objects
|
41
|
+
def subscriptions
|
42
|
+
__getobj__
|
43
|
+
end
|
44
|
+
|
45
|
+
# Attach a new object to listen for events. A listener is expected to be
|
46
|
+
# call-able, and it will receive the `#call` message with an {Event} each
|
47
|
+
# time one is emitted.
|
48
|
+
# @param new_listener [:call]
|
49
|
+
# @return [Subscription] a subscription object which can be used to
|
50
|
+
# cancel the subscription.
|
51
|
+
def attach(new_listener)
|
52
|
+
FullSubscription.new(self, new_listener).tap do |subscription|
|
53
|
+
self << subscription
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Detach a listener. This locates the subscription corresponding to the
|
58
|
+
# given listener (if any), and removes it.
|
59
|
+
# @param [:call] listener a listener to be unsubscribed
|
60
|
+
# @return [void]
|
61
|
+
def detach(listener)
|
62
|
+
delete(FullSubscription.new(self, listener))
|
63
|
+
end
|
64
|
+
|
65
|
+
# Attach blocks of code to handle specific named events.
|
66
|
+
# @overload on(name, &block)
|
67
|
+
# Attach a block to be called for a specific event. The block will be
|
68
|
+
# called with the event arguments (not the event object).
|
69
|
+
# @param name [Symbol]
|
70
|
+
# @param block [Proc] what to do when the event is emitted
|
71
|
+
# @overload on(handlers)
|
72
|
+
# Attach multiple event-specific handlers at once.
|
73
|
+
# @param handlers [Hash{Symbol => [:call]}] a map of event names to
|
74
|
+
# callable handlers.
|
75
|
+
# @return (see #attach)
|
76
|
+
def on(name_or_handlers, &block)
|
77
|
+
case name_or_handlers
|
78
|
+
when Symbol
|
79
|
+
attach_to_single_event(name_or_handlers, block)
|
80
|
+
when Hash
|
81
|
+
attach(OpenObserver.new(name_or_handlers))
|
82
|
+
else
|
83
|
+
fail ArgumentError, "Event name or Hash required"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Emit an event to be distributed to all interested listeners.
|
88
|
+
# @param event_name [Symbol] the name of the event
|
89
|
+
# @param extra_args [Array] any extra arguments that should accompany the
|
90
|
+
# event
|
91
|
+
# @return the notifier's result value
|
92
|
+
def emit(event_name, *extra_args)
|
93
|
+
notifier = @notifier_maker.call
|
94
|
+
each do |subscription|
|
95
|
+
event = Event.new(event_name, @event_source, extra_args)
|
96
|
+
notifier.notify(subscription, event)
|
97
|
+
end
|
98
|
+
notifier.result
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def attach_to_single_event(event_name, block)
|
104
|
+
SingleEventSubscription.new(self, block, event_name).tap do
|
105
|
+
|subscription|
|
106
|
+
self << subscription
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|