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,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
@@ -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,10 @@
1
+ require "brainguy"
2
+
3
+ events = Brainguy::Emitter.new
4
+ observer = proc do |event|
5
+ puts "Got event: #{event.name}"
6
+ end
7
+ events.attach(observer)
8
+ events.emit(:ding)
9
+
10
+ # >> Got event: ding
@@ -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!
@@ -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