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