brainguy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|