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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a5ee4639149ae25d01b264ed4b9c95195362945f
4
+ data.tar.gz: e01b8fa15ad59f03df5a6e5844e21eead9cd1b81
5
+ SHA512:
6
+ metadata.gz: c0ccb42305475236a4291b1b3d32236b84ff3297628f0817271ca68c99bc789501b1b7c5d658dd38cea86fe03c2933fd6eff67921d7acaefe740da55405efdd4
7
+ data.tar.gz: f92036dd02022d50319c71a5f65ffdb71387636bcde316b47217595d0165cd4a076808d52d66c8d482a883d45bc2225baf2633c62a9200c86eacaaad5fcf611d
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /.idea/
@@ -0,0 +1,4 @@
1
+ --exclude README\\.markdown\\.erb
2
+ --markup markdown
3
+ -
4
+ README.markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in brainguy.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Avdi Grimm
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,345 @@
1
+ # Brainguy
2
+
3
+ ![Observer, AKA "Brain Guy"](http://static.tvtropes.org/pmwiki/pub/images/MST3K_Brain_Guy_7093.jpg)
4
+
5
+ Brainguy is an Observer library for Ruby.
6
+
7
+ ## Synopsis
8
+
9
+ ```ruby
10
+ <%= File.read("examples/synopsis.rb") %>
11
+ ```
12
+
13
+ ## Introduction
14
+
15
+ *Well, here we are again.*
16
+
17
+ Back with another of those block-rockin' READMEs!
18
+
19
+ *You know, I can just leave now.*
20
+
21
+ Sorry. It won't happen again.
22
+
23
+ *So, "Brainguy", huh. What's the deal this time?*
24
+
25
+ This is an Observer pattern library for Ruby. The name is a play on the
26
+ character from Mystery Sci---
27
+
28
+ *Yeah yeah blah blah nerd nerd very clever. What's it do?*
29
+
30
+ In a nutshell, it's a decoupling mechanism. It lets "observer" objects
31
+ subscribe to events generated by other objects.
32
+
33
+ *Kind of like the `observer` Ruby standard library?"*
34
+
35
+ Yeah, exactly. But this library is a little bit fancier. It adds a
36
+ number of conveniences that you otherwise might have to build yourself on top of `observer`.
37
+
38
+ *Such as?*
39
+
40
+ Well, the most important feature it has is *named event types*. Instead of a single "update" event, events have symbolic names. Observers can choose which events they care about, and ignore the rest.
41
+
42
+ ### Defining some terms
43
+
44
+ *What exactly is a "observer"? Is it a special kind of object?*
45
+
46
+ Not really, no. Fundamentally a observer is any object which responds to `#call`. The most obvious example of such an object is a `Proc`. Here's an example of using a proc as a simple observer:
47
+
48
+ ```ruby
49
+ <%= File.read("examples/proc_observer.rb") %>
50
+ ```
51
+
52
+ Every time the emitter emits an event, the observer proc will receive `#call` with an `Event` object as an argument.
53
+
54
+ *What's an "emitter"?*
55
+
56
+ An Emitter serves dual roles: first, it manages subscriptions to a particular event source. And second, it can "emit" events to all of the observers currently subscribed to it.
57
+
58
+ *What exactly is an "event", anyway?*
59
+
60
+ Notionally an event is some occurrence in an object, which other objects might want to know about. What sort of occurrences might be depends on your problem domain. A `User` might have a `:modified` event. An `WebServiceRequest` might have a `:success` event. A `Toaster` might have a `:pop` event. And so on.
61
+
62
+ *So an event is just a symbol?*
63
+
64
+ An event is *named* with a symbol. But there is some other information that normally travels along with an event:
65
+
66
+ - An event *source*, which is the observer object that generated the event.
67
+ - An arbitrary list of *arguments*.
68
+
69
+ Extra arguments can be added to an event by passing extra arguments to the `#emit`, like this:
70
+
71
+ ```ruby
72
+ events.emit(:movie_sign, movie_title: "Giant Spider Invasion")
73
+ ```
74
+
75
+ For convenience, the event name, source, and arguments are all bundled into an `Event` object before being disseminated to observers.
76
+
77
+ ### Making an object observable
78
+
79
+ *OK, say I have an object that I want to make observable. How would I go about that?*
80
+
81
+ Well, the no-magic way might go something like this:
82
+
83
+ ```ruby
84
+ <%= File.read("examples/manual_observable.rb") %>
85
+ ```
86
+
87
+ Notice that we pass `self` to the new `Emitter`, so that it will know what object to set as the event source for emitted events.
88
+
89
+ *That's pretty straightforward. Is there a more-magic way?*
90
+
91
+ Of course! But it's not much more magic. There's an `Observable` module that just packages up the convention we used above into a reusable mixin you can use in any of your classes. Here's what that code would look like using the mixin:
92
+
93
+ ```ruby
94
+ <%= File.read("examples/include_observable.rb") %>
95
+ ```
96
+
97
+ *I see that instead of `events.emit(...)`, now the class just uses `emit(...)`. And the same with `#on`.*
98
+
99
+ Very observant! `Observable` adds four methods to classes which mix it in:
100
+
101
+ - `#on`, to quickly attach single-event handlers on the object.
102
+ - `#emit`, a private method for conveniently emitting events inside the class.
103
+ - `#events`, to access the `Emitter` object.
104
+ - `#with_subscription_scope`, which we'll talk about later.
105
+
106
+ *That's not a lot of methods added.*
107
+
108
+ Nope! That's intentional. These are your classes, and I don't want to clutter up your API unnecessarily. `#on` and `#emit` are provided as conveniences for common actions. Anything else you need, you can get to via the `Emitter` returned from `#events`.
109
+
110
+ ### Constraining event types
111
+
112
+ *I see that un-handled events are just ignored. Doesn't that make it easy to miss events because of a typo in the name?*
113
+
114
+ Yeah, it kinda does. In order to help with that, there's an alternative kind of emitter: a `ManifestEmitter`. And to go along with it, there's a `ManifestlyObservable` mixin module. We customize the module with a list of known event names. Then if anything tries to either emit or subscribe to an unknown event name, the emitter outputs a warning.
115
+
116
+ Well, that's what it does by default. We can also customize the policy for how to handle unknown events, as this example demonstrates:
117
+
118
+ ```ruby
119
+ <%= File.read("examples/include_manifestly_observable.rb") %>
120
+ ```
121
+
122
+ ### All about observers
123
+
124
+ *I'm still a little confused about `#on`. Is that just another way to add an observer?*
125
+
126
+ `#on` is really just a shortcut. Often we don't want to attach a whole observer to an observable object. We just want to trigger a particular block of code to be run when a specific event is detected. So `#on` makes it easy to hook up a block of code to a single event.
127
+
128
+ *So it's a special case.*
129
+
130
+ Yep!
131
+
132
+ *Let's talk about the general case a bit more. You said an observer is just a callable object?*
133
+
134
+ Yeah. Anything which will respond to `#call` and accept a single `Event` as an argument.
135
+
136
+ *But what if I want my observer to do different things depending on what kind of event it receives? Do I have to write a case statement inside my `#call` method?*
137
+
138
+ You could if you wanted to. But that's a common desire, so there are some conveniences for it.
139
+
140
+ *Such as...?*
141
+
142
+ Well, first off, there's `OpenObserver`. It's kinda like Ruby's `OpenObject`, but for observer objects. You can use it to quickly put together a reusable observer object. For instance, here's an example where we have two different observable objects, observed by a single `OpenObserver`.
143
+
144
+ ```ruby
145
+ <%= File.read("examples/open_observer.rb") %>
146
+ ```
147
+
148
+ There are a few other ways to instantiate an `OpenObserver`; check out the source code and tests for more information.
149
+
150
+ *What if my observer needs are more elaborate? What if I want a dedicated class for observing an event stream?*
151
+
152
+ There's a helper for that as well. Here's an example where we have a `Poem` class that can recite a poem, generating events along the way. And then we have an `HtmlFormatter` which observes those events and incrementally constructs some HTML text as it does so.
153
+
154
+ ```ruby
155
+ <%= File.read("examples/include_observer.rb") %>
156
+ ```
157
+
158
+ *So including `Observer` automatically handles the dispatching of events from `#call` to the various `#on_` methods?*
159
+
160
+ Yes, exactly. And through some metaprogramming, it is able to do this in a way that is just as performant as a hand-written case statement.
161
+
162
+ *How do you know it's that fast?*
163
+
164
+ You can run the proof-of-concept benchmark for yourself! It's in the `scripts` directory.
165
+
166
+ ### Managing subscription lifetime
167
+
168
+ *You know, it occurs to me that in the `Poem` example, it really doesn't make sense to have an `HtmlFormatter` plugged into a `Poem` forever. Is there a way to attach it before the call to `#recite`, and then detach it immediately after?*
169
+
170
+ Of course. All listener registration methods return a `Subscription` object which can be used to manage the subscription of an observer to emitter. If we wanted to observe the `Poem` for just a single recital, we could do it like this:
171
+
172
+ ```ruby
173
+ p = Poem.new
174
+ f = HtmlFormatter.new
175
+ subscription = p.events.attach(f)
176
+ p.recite
177
+ subscription.cancel
178
+ ```
179
+
180
+ *OK, so I just need to remember to `#cancel` the subscriptions that I don't want sticking around.*
181
+
182
+ That's one way to do it. But this turns out to be a common use case. It's often desirable to have observers that are in effect just for the length of a single method call.
183
+
184
+ Here's how we might re-write the "poem" example with event subscriptions scoped to just the `#recite` call:
185
+
186
+ ```ruby
187
+ <%= File.read("examples/scoped_subscription.rb") %>
188
+ ```
189
+
190
+ In this example, the `HtmlFormatter` is only subscribed to poem events for the duration of the call to `#recite`. After that it is automatically detached.
191
+
192
+ ### Replacing return values with events
193
+
194
+ *Interesting. I can see this being useful for more than just traditionally event-generating objects.*
195
+
196
+ Indeed it is! This turns out to be a useful pattern for any kind of method which acts as a "command".
197
+
198
+ For instance, let's imagine a fictional HTTP request method. Different things happen over the course of a request:
199
+
200
+ - headers come back
201
+ - data comes back (possibly more than once, if it is a streaming-style connection)
202
+ - an error may occur
203
+ - otherwise, at some point it will reach a successful finish
204
+
205
+ Let's look at how that could be modeled using an "event-ful" method:
206
+
207
+ ```ruby
208
+ connection.request(:get, "/") do |events|
209
+ events.on(:header){ ... } # handle a header
210
+ events.on(:data){ ... } # handle data
211
+ events.on(:error){ ... } # handle errors
212
+ events.on(:success){ ... } # finish up
213
+ end
214
+ ```
215
+
216
+ This API has some interesting properties:
217
+
218
+ - Notice how some of the events that are handled will only occur once (`error`, `success`), whereas others (`data`, `header`) may be called multiple times. The event-handler API style means that both singular and repeatable events can be handled in a consistent way.
219
+ - A common headache in designing APIs is deciding how to handle errors. Should an exception be raised? Should there be an exceptional return value? Using events, the client code can set the error policy.
220
+
221
+ *But couldn't you accomplish the same thing by returning different values for success, failure, etc?*
222
+
223
+ Not easily. Sure, you could define a method that returned `[:success, 200]` on success, and `[:error, 500]` on failure. But what about the `data` events that may be emitted multiple times as data comes in? Libraries typically handle this limitation by providing separate APIs and/or objects for "streaming" responses. Using events handlers makes it possible to handle both single-return and streaming-style requests in a consistent way.
224
+
225
+ *I don't like that blocks-in-a-block syntax*
226
+
227
+ If you're willing to wait until a method call is complete before handling events, there's an alternative to that syntax. Let's say our `#request` method is implemented something like this:
228
+
229
+ ```ruby
230
+ class Connection
231
+ include Brainguy::Observable
232
+
233
+ def request(method, path, &block)
234
+ with_subscription_scope(block) do
235
+ # ...
236
+ end
237
+ end
238
+
239
+ # ...
240
+ end
241
+ ```
242
+
243
+ In that case, instead of sending it with a block, we can do this:
244
+
245
+ ```ruby
246
+ connection.request(:get, "/")
247
+ .on(:header){ ... } # handle a header
248
+ .on(:data){ ... } # handle data
249
+ .on(:error){ ... } # handle errors
250
+ .on(:success){ ... } # finish up
251
+ ```
252
+
253
+ *How the heck does that work?*
254
+
255
+ If the method is called without a block, events are queued up in an {Brainguy::IdempotentEmitter}. This is a special kind of emitter that "plays back" any events that an observer missed, as soon as it is attached.
256
+
257
+ Then it's wrapped in a special {Brainguy::FluentEmitter} before being returned. This enables the "chained" calling style you can see in the example above. Normally, sending `#on` would return a {Brainguy::Subscription} object, so that wouldn't work.
258
+
259
+ The upshot is that all the events are collected over the course of the method's execution. Then they are played back on each handler as it is added.
260
+
261
+ *What if I **only** want eventful methods? I don't want my object to carry a long-lived list of observers around?*
262
+
263
+ Gotcha covered. You can use {Brainguy.with_subscription_scope} to add a temporary subscription scope to any method without first including {Brainguy::Observable}.
264
+
265
+ ```ruby
266
+ class Connection
267
+ def request(method, path, &block)
268
+ Brainguy.with_subscription_scope(self) do
269
+ # ...
270
+ end
271
+ end
272
+
273
+ # ...
274
+ end
275
+ ```
276
+
277
+ *This is a lot to take in. Anything else you want to tell me about?*
278
+
279
+ We've covered most of the major features. One thing we haven't talked about is error suppression.
280
+
281
+ ### Suppressing errors
282
+
283
+ *Why would you want to suppress errors?*
284
+
285
+ Well, we all know that observers effect the thing being observed. But it can be nice to minimize that effect as much as possible. For instance, if you have a critical process that's being observed, you may want to ensure that spurious errors inside of observers don't cause it to crash.
286
+
287
+ *Yeah, I could see where that could be a problem.*
288
+
289
+ So there are some tools for setting a policy in place for what to do with errors in event handlers, including turning them into warnings, suppressing them entirely, or building a list of errors.
290
+
291
+ I'm not going to go over them in detail here in the README, but you should check out {Brainguy::ErrorHandlingNotifier} and {Brainguy::ErrorCollectingNotifier}, along with their spec files, for more information. They are pretty easy to use.
292
+
293
+ ## FAQ
294
+
295
+ *Is this library like ActiveRecord callbacks? Or like Rails observers?*
296
+
297
+ No. ActiveRecord enables callbacks to be enabled at a class level, so that every instance is implicitly being subscribed to. Rails "observers" enable observers to be added with no knowledge whatsoever on the part of the objects being observed.
298
+
299
+ Brainguy explicitly eschews this kind of "spooky action at a distance". If you want to be notified of what goes on inside an object, you have to subscribe to that object.
300
+
301
+ *Is this an aspect-oriented programming library? Or a lisp-style "method advice" system?*
302
+
303
+ No. Aspect-oriented programming and lisp-style method advice are more ways of adding "spooky action at a distance", where the code being advised may have no idea that it is having foreign logic attached to it. As well as no control over *where* that foreign logic is applied.
304
+
305
+ In Brainguy, by contrast, objects are explicitly subscribed-to, and events are explicitly emitted.
306
+
307
+ *Is this a library for "Reactive Programming"?*
308
+
309
+ Not in and of itself. It could potentially serve as the foundation for such a library though.
310
+
311
+ *Is this a library for creating "hooks"?*
312
+
313
+ Sort of. Observers do let you "hook" arbitrary handlers to events in other objects. However, this library is not aimed at enabling you to create hooks that *modify* the behavior of other methods. It's primarily intended to allow objects to be *notified* of significant events, without interfering in the processing of the object sending out the notifications.
314
+
315
+ *Is this an asynchronous messaging or reactor system?*
316
+
317
+ No. Brainguy events are processed synchronously have no awareness of concurrency.
318
+
319
+ ## Installation
320
+
321
+ Add this line to your application's Gemfile:
322
+
323
+ ```ruby
324
+ gem 'brainguy'
325
+ ```
326
+
327
+ And then execute:
328
+
329
+ $ bundle
330
+
331
+ Or install it yourself as:
332
+
333
+ $ gem install brainguy
334
+
335
+ ## Usage
336
+
337
+ Coming soon!
338
+
339
+ ## Contributing
340
+
341
+ 1. Fork it ( https://github.com/avdi/brainguy/fork )
342
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
343
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
344
+ 4. Push to the branch (`git push origin my-new-feature`)
345
+ 5. Create a new Pull Request
@@ -0,0 +1,579 @@
1
+ # Brainguy
2
+
3
+ ![Observer, AKA "Brain Guy"](http://static.tvtropes.org/pmwiki/pub/images/MST3K_Brain_Guy_7093.jpg)
4
+
5
+ Brainguy is an Observer library for Ruby.
6
+
7
+ ## Synopsis
8
+
9
+ ```ruby
10
+ require "brainguy"
11
+
12
+ class SatelliteOfLove
13
+ include Brainguy::Observable
14
+
15
+ def intro_song
16
+ emit(:robot_roll_call)
17
+ end
18
+
19
+ def send_the_movie
20
+ emit(:movie_sign)
21
+ end
22
+ end
23
+
24
+ class Crew
25
+ include Brainguy::Observer
26
+ end
27
+
28
+ class TomServo < Crew
29
+ def on_robot_roll_call(event)
30
+ puts "Tom: Check me out!"
31
+ end
32
+ end
33
+
34
+ class CrowTRobot < Crew
35
+ def on_robot_roll_call(event)
36
+ puts "Crow: I'm different!"
37
+ end
38
+ end
39
+
40
+ class MikeNelson < Crew
41
+ def on_movie_sign(event)
42
+ puts "Mike: Oh no we've got movie sign!"
43
+ end
44
+ end
45
+
46
+ sol = SatelliteOfLove.new
47
+ # Attach specific event handlers without a listener object
48
+ sol.on(:robot_roll_call) do
49
+ puts "[Robot roll call!]"
50
+ end
51
+ sol.on(:movie_sign) do
52
+ puts "[Movie sign flashes]"
53
+ end
54
+ sol.events.attach TomServo.new
55
+ sol.events.attach CrowTRobot.new
56
+ sol.events.attach MikeNelson.new
57
+
58
+ sol.intro_song
59
+ sol.send_the_movie
60
+
61
+ # >> [Robot roll call!]
62
+ # >> Tom: Check me out!
63
+ # >> Crow: I'm different!
64
+ # >> [Movie sign flashes]
65
+ # >> Mike: Oh no we've got movie sign!
66
+
67
+ ```
68
+
69
+ ## Introduction
70
+
71
+ *Well, here we are again.*
72
+
73
+ Back with another of those block-rockin' READMEs!
74
+
75
+ *You know, I can just leave now.*
76
+
77
+ Sorry. It won't happen again.
78
+
79
+ *So, "Brainguy", huh. What's the deal this time?*
80
+
81
+ This is an Observer pattern library for Ruby. The name is a play on the
82
+ character from Mystery Sci---
83
+
84
+ *Yeah yeah blah blah nerd nerd very clever. What's it do?*
85
+
86
+ In a nutshell, it's a decoupling mechanism. It lets "observer" objects
87
+ subscribe to events generated by other objects.
88
+
89
+ *Kind of like the `observer` Ruby standard library?"*
90
+
91
+ Yeah, exactly. But this library is a little bit fancier. It adds a
92
+ number of conveniences that you otherwise might have to build yourself on top of `observer`.
93
+
94
+ *Such as?*
95
+
96
+ Well, the most important feature it has is *named event types*. Instead of a single "update" event, events have symbolic names. Observers can choose which events they care about, and ignore the rest.
97
+
98
+ ### Defining some terms
99
+
100
+ *What exactly is a "observer"? Is it a special kind of object?*
101
+
102
+ Not really, no. Fundamentally a observer is any object which responds to `#call`. The most obvious example of such an object is a `Proc`. Here's an example of using a proc as a simple observer:
103
+
104
+ ```ruby
105
+ require "brainguy"
106
+
107
+ events = Brainguy::Emitter.new
108
+ observer = proc do |event|
109
+ puts "Got event: #{event.name}"
110
+ end
111
+ events.attach(observer)
112
+ events.emit(:ding)
113
+
114
+ # >> Got event: ding
115
+
116
+ ```
117
+
118
+ Every time the emitter emits an event, the observer proc will receive `#call` with an `Event` object as an argument.
119
+
120
+ *What's an "emitter"?*
121
+
122
+ An Emitter serves dual roles: first, it manages subscriptions to a particular event source. And second, it can "emit" events to all of the observers currently subscribed to it.
123
+
124
+ *What exactly is an "event", anyway?*
125
+
126
+ Notionally an event is some occurrence in an object, which other objects might want to know about. What sort of occurrences might be depends on your problem domain. A `User` might have a `:modified` event. An `WebServiceRequest` might have a `:success` event. A `Toaster` might have a `:pop` event. And so on.
127
+
128
+ *So an event is just a symbol?*
129
+
130
+ An event is *named* with a symbol. But there is some other information that normally travels along with an event:
131
+
132
+ - An event *source*, which is the observer object that generated the event.
133
+ - An arbitrary list of *arguments*.
134
+
135
+ Extra arguments can be added to an event by passing extra arguments to the `#emit`, like this:
136
+
137
+ ```ruby
138
+ events.emit(:movie_sign, movie_title: "Giant Spider Invasion")
139
+ ```
140
+
141
+ For convenience, the event name, source, and arguments are all bundled into an `Event` object before being disseminated to observers.
142
+
143
+ ### Making an object observable
144
+
145
+ *OK, say I have an object that I want to make observable. How would I go about that?*
146
+
147
+ Well, the no-magic way might go something like this:
148
+
149
+ ```ruby
150
+ require "brainguy"
151
+
152
+ class Toaster
153
+ attr_reader :events
154
+
155
+ def initialize
156
+ @events = Brainguy::Emitter.new(self)
157
+ end
158
+
159
+ def make_toast
160
+ events.emit(:start)
161
+ events.emit(:pop)
162
+ end
163
+ end
164
+
165
+ toaster = Toaster.new
166
+ toaster.events.on(:pop) do
167
+ puts "Toanst is done!"
168
+ end
169
+ toaster.make_toast
170
+
171
+ # >> Toast is done!
172
+
173
+ ```
174
+
175
+ Notice that we pass `self` to the new `Emitter`, so that it will know what object to set as the event source for emitted events.
176
+
177
+ *That's pretty straightforward. Is there a more-magic way?*
178
+
179
+ Of course! But it's not much more magic. There's an `Observable` module that just packages up the convention we used above into a reusable mixin you can use in any of your classes. Here's what that code would look like using the mixin:
180
+
181
+ ```ruby
182
+ require "brainguy"
183
+
184
+ class Toaster
185
+ include Brainguy::Observable
186
+
187
+ def make_toast
188
+ emit(:start)
189
+ emit(:pop)
190
+ end
191
+ end
192
+
193
+ toaster = Toaster.new
194
+ toaster.on(:pop) do
195
+ puts "Toast is done!"
196
+ end
197
+ toaster.make_toast
198
+
199
+ # >> Toast is done!
200
+
201
+ ```
202
+
203
+ *I see that instead of `events.emit(...)`, now the class just uses `emit(...)`. And the same with `#on`.*
204
+
205
+ Very observant! `Observable` adds four methods to classes which mix it in:
206
+
207
+ - `#on`, to quickly attach single-event handlers on the object.
208
+ - `#emit`, a private method for conveniently emitting events inside the class.
209
+ - `#events`, to access the `Emitter` object.
210
+ - `#with_subscription_scope`, which we'll talk about later.
211
+
212
+ *That's not a lot of methods added.*
213
+
214
+ Nope! That's intentional. These are your classes, and I don't want to clutter up your API unnecessarily. `#on` and `#emit` are provided as conveniences for common actions. Anything else you need, you can get to via the `Emitter` returned from `#events`.
215
+
216
+ ### Constraining event types
217
+
218
+ *I see that un-handled events are just ignored. Doesn't that make it easy to miss events because of a typo in the name?*
219
+
220
+ Yeah, it kinda does. In order to help with that, there's an alternative kind of emitter: a `ManifestEmitter`. And to go along with it, there's a `ManifestlyObservable` mixin module. We customize the module with a list of known event names. Then if anything tries to either emit or subscribe to an unknown event name, the emitter outputs a warning.
221
+
222
+ Well, that's what it does by default. We can also customize the policy for how to handle unknown events, as this example demonstrates:
223
+
224
+ ```ruby
225
+ require "brainguy"
226
+
227
+ class Toaster
228
+ include Brainguy::ManifestlyObservable.new(:start, :pop)
229
+
230
+ def make_toast
231
+ emit(:start)
232
+ emit(:lop)
233
+ end
234
+ end
235
+
236
+ toaster = Toaster.new
237
+ toaster.events.unknown_event_policy = :raise_error
238
+ toaster.on(:plop) do
239
+ puts "Toast is done!"
240
+ end
241
+ toaster.make_toast
242
+
243
+ # ~> Brainguy::UnknownEvent
244
+ # ~> #on received for unknown event type 'plop'
245
+ # ~>
246
+ # ~> xmptmp-in27856uxq.rb:14:in `<main>'
247
+
248
+ ```
249
+
250
+ ### All about observers
251
+
252
+ *I'm still a little confused about `#on`. Is that just another way to add an observer?*
253
+
254
+ `#on` is really just a shortcut. Often we don't want to attach a whole observer to an observable object. We just want to trigger a particular block of code to be run when a specific event is detected. So `#on` makes it easy to hook up a block of code to a single event.
255
+
256
+ *So it's a special case.*
257
+
258
+ Yep!
259
+
260
+ *Let's talk about the general case a bit more. You said an observer is just a callable object?*
261
+
262
+ Yeah. Anything which will respond to `#call` and accept a single `Event` as an argument.
263
+
264
+ *But what if I want my observer to do different things depending on what kind of event it receives? Do I have to write a case statement inside my `#call` method?*
265
+
266
+ You could if you wanted to. But that's a common desire, so there are some conveniences for it.
267
+
268
+ *Such as...?*
269
+
270
+ Well, first off, there's `OpenObserver`. It's kinda like Ruby's `OpenObject`, but for observer objects. You can use it to quickly put together a reusable observer object. For instance, here's an example where we have two different observable objects, observed by a single `OpenObserver`.
271
+
272
+ ```ruby
273
+ require "brainguy"
274
+
275
+ class VideoRender
276
+ include Brainguy::Observable
277
+ attr_reader :name
278
+ def initialize(name)
279
+ @name = name
280
+ end
281
+
282
+ def do_render
283
+ emit(:complete)
284
+ end
285
+ end
286
+
287
+ v1 = VideoRender.new("foo.mp4")
288
+ v2 = VideoRender.new("bar.mp4")
289
+
290
+ observer = Brainguy::OpenObserver.new do |o|
291
+ o.on_complete do |event|
292
+ puts "Video #{event.source.name} is done rendering!"
293
+ end
294
+ end
295
+
296
+ v1.events.attach(observer)
297
+ v2.events.attach(observer)
298
+
299
+ v1.do_render
300
+ v2.do_render
301
+
302
+ # >> Video foo.mp4 is done rendering!
303
+ # >> Video bar.mp4 is done rendering!
304
+
305
+ ```
306
+
307
+ There are a few other ways to instantiate an `OpenObserver`; check out the source code and tests for more information.
308
+
309
+ *What if my observer needs are more elaborate? What if I want a dedicated class for observing an event stream?*
310
+
311
+ There's a helper for that as well. Here's an example where we have a `Poem` class that can recite a poem, generating events along the way. And then we have an `HtmlFormatter` which observes those events and incrementally constructs some HTML text as it does so.
312
+
313
+ ```ruby
314
+ require "brainguy"
315
+
316
+ class Poem
317
+ include Brainguy::Observable
318
+ def recite
319
+ emit(:title, "Jabberwocky")
320
+ emit(:line, "'twas brillig, and the slithy toves")
321
+ emit(:line, "Did gyre and gimbal in the wabe")
322
+ end
323
+ end
324
+
325
+ class HtmlFormatter
326
+ include Brainguy::Observer
327
+
328
+ attr_reader :result
329
+
330
+ def initialize
331
+ @result = ""
332
+ end
333
+
334
+ def on_title(event)
335
+ @result << "<h1>#{event.args.first}</h1>"
336
+ end
337
+
338
+ def on_line(event)
339
+ @result << "#{event.args.first}</br>"
340
+ end
341
+ end
342
+
343
+ p = Poem.new
344
+ f = HtmlFormatter.new
345
+ p.events.attach(f)
346
+ p.recite
347
+
348
+ f.result
349
+ # => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
350
+
351
+ ```
352
+
353
+ *So including `Observer` automatically handles the dispatching of events from `#call` to the various `#on_` methods?*
354
+
355
+ Yes, exactly. And through some metaprogramming, it is able to do this in a way that is just as performant as a hand-written case statement.
356
+
357
+ *How do you know it's that fast?*
358
+
359
+ You can run the proof-of-concept benchmark for yourself! It's in the `scripts` directory.
360
+
361
+ ### Managing subscription lifetime
362
+
363
+ *You know, it occurs to me that in the `Poem` example, it really doesn't make sense to have an `HtmlFormatter` plugged into a `Poem` forever. Is there a way to attach it before the call to `#recite`, and then detach it immediately after?*
364
+
365
+ Of course. All listener registration methods return a `Subscription` object which can be used to manage the subscription of an observer to emitter. If we wanted to observe the `Poem` for just a single recital, we could do it like this:
366
+
367
+ ```ruby
368
+ p = Poem.new
369
+ f = HtmlFormatter.new
370
+ subscription = p.events.attach(f)
371
+ p.recite
372
+ subscription.cancel
373
+ ```
374
+
375
+ *OK, so I just need to remember to `#cancel` the subscriptions that I don't want sticking around.*
376
+
377
+ That's one way to do it. But this turns out to be a common use case. It's often desirable to have observers that are in effect just for the length of a single method call.
378
+
379
+ Here's how we might re-write the "poem" example with event subscriptions scoped to just the `#recite` call:
380
+
381
+ ```ruby
382
+ require "brainguy"
383
+
384
+ class Poem
385
+ include Brainguy::Observable
386
+ def recite(&block)
387
+ with_subscription_scope(block) do
388
+ emit(:title, "Jabberwocky")
389
+ emit(:line, "'twas brillig, and the slithy toves")
390
+ emit(:line, "Did gyre and gimbal in the wabe")
391
+ end
392
+ end
393
+ end
394
+
395
+ class HtmlFormatter
396
+ include Brainguy::Observer
397
+
398
+ attr_reader :result
399
+
400
+ def initialize
401
+ @result = ""
402
+ end
403
+
404
+ def on_title(event)
405
+ @result << "<h1>#{event.args.first}</h1>"
406
+ end
407
+
408
+ def on_line(event)
409
+ @result << "#{event.args.first}</br>"
410
+ end
411
+ end
412
+
413
+ p = Poem.new
414
+ f = HtmlFormatter.new
415
+ p.recite do |events|
416
+ events.attach(f)
417
+ end
418
+
419
+ f.result
420
+ # => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
421
+
422
+ ```
423
+
424
+ In this example, the `HtmlFormatter` is only subscribed to poem events for the duration of the call to `#recite`. After that it is automatically detached.
425
+
426
+ ### Replacing return values with events
427
+
428
+ *Interesting. I can see this being useful for more than just traditionally event-generating objects.*
429
+
430
+ Indeed it is! This turns out to be a useful pattern for any kind of method which acts as a "command".
431
+
432
+ For instance, let's imagine a fictional HTTP request method. Different things happen over the course of a request:
433
+
434
+ - headers come back
435
+ - data comes back (possibly more than once, if it is a streaming-style connection)
436
+ - an error may occur
437
+ - otherwise, at some point it will reach a successful finish
438
+
439
+ Let's look at how that could be modeled using an "event-ful" method:
440
+
441
+ ```ruby
442
+ connection.request(:get, "/") do |events|
443
+ events.on(:header){ ... } # handle a header
444
+ events.on(:data){ ... } # handle data
445
+ events.on(:error){ ... } # handle errors
446
+ events.on(:success){ ... } # finish up
447
+ end
448
+ ```
449
+
450
+ This API has some interesting properties:
451
+
452
+ - Notice how some of the events that are handled will only occur once (`error`, `success`), whereas others (`data`, `header`) may be called multiple times. The event-handler API style means that both singular and repeatable events can be handled in a consistent way.
453
+ - A common headache in designing APIs is deciding how to handle errors. Should an exception be raised? Should there be an exceptional return value? Using events, the client code can set the error policy.
454
+
455
+ *But couldn't you accomplish the same thing by returning different values for success, failure, etc?*
456
+
457
+ Not easily. Sure, you could define a method that returned `[:success, 200]` on success, and `[:error, 500]` on failure. But what about the `data` events that may be emitted multiple times as data comes in? Libraries typically handle this limitation by providing separate APIs and/or objects for "streaming" responses. Using events handlers makes it possible to handle both single-return and streaming-style requests in a consistent way.
458
+
459
+ *I don't like that blocks-in-a-block syntax*
460
+
461
+ If you're willing to wait until a method call is complete before handling events, there's an alternative to that syntax. Let's say our `#request` method is implemented something like this:
462
+
463
+ ```ruby
464
+ class Connection
465
+ include Brainguy::Observable
466
+
467
+ def request(method, path, &block)
468
+ with_subscription_scope(block) do
469
+ # ...
470
+ end
471
+ end
472
+
473
+ # ...
474
+ end
475
+ ```
476
+
477
+ In that case, instead of sending it with a block, we can do this:
478
+
479
+ ```ruby
480
+ connection.request(:get, "/")
481
+ .on(:header){ ... } # handle a header
482
+ .on(:data){ ... } # handle data
483
+ .on(:error){ ... } # handle errors
484
+ .on(:success){ ... } # finish up
485
+ ```
486
+
487
+ *How the heck does that work?*
488
+
489
+ If the method is called without a block, events are queued up in an {Brainguy::IdempotentEmitter}. This is a special kind of emitter that "plays back" any events that an observer missed, as soon as it is attached.
490
+
491
+ Then it's wrapped in a special {Brainguy::FluentEmitter} before being returned. This enables the "chained" calling style you can see in the example above. Normally, sending `#on` would return a {Brainguy::Subscription} object, so that wouldn't work.
492
+
493
+ The upshot is that all the events are collected over the course of the method's execution. Then they are played back on each handler as it is added.
494
+
495
+ *What if I **only** want eventful methods? I don't want my object to carry a long-lived list of observers around?*
496
+
497
+ Gotcha covered. You can use {Brainguy.with_subscription_scope} to add a temporary subscription scope to any method without first including {Brainguy::Observable}.
498
+
499
+ ```ruby
500
+ class Connection
501
+ def request(method, path, &block)
502
+ Brainguy.with_subscription_scope(self) do
503
+ # ...
504
+ end
505
+ end
506
+
507
+ # ...
508
+ end
509
+ ```
510
+
511
+ *This is a lot to take in. Anything else you want to tell me about?*
512
+
513
+ We've covered most of the major features. One thing we haven't talked about is error suppression.
514
+
515
+ ### Suppressing errors
516
+
517
+ *Why would you want to suppress errors?*
518
+
519
+ Well, we all know that observers effect the thing being observed. But it can be nice to minimize that effect as much as possible. For instance, if you have a critical process that's being observed, you may want to ensure that spurious errors inside of observers don't cause it to crash.
520
+
521
+ *Yeah, I could see where that could be a problem.*
522
+
523
+ So there are some tools for setting a policy in place for what to do with errors in event handlers, including turning them into warnings, suppressing them entirely, or building a list of errors.
524
+
525
+ I'm not going to go over them in detail here in the README, but you should check out {Brainguy::ErrorHandlingNotifier} and {Brainguy::ErrorCollectingNotifier}, along with their spec files, for more information. They are pretty easy to use.
526
+
527
+ ## FAQ
528
+
529
+ *Is this library like ActiveRecord callbacks? Or like Rails observers?*
530
+
531
+ No. ActiveRecord enables callbacks to be enabled at a class level, so that every instance is implicitly being subscribed to. Rails "observers" enable observers to be added with no knowledge whatsoever on the part of the objects being observed.
532
+
533
+ Brainguy explicitly eschews this kind of "spooky action at a distance". If you want to be notified of what goes on inside an object, you have to subscribe to that object.
534
+
535
+ *Is this an aspect-oriented programming library? Or a lisp-style "method advice" system?*
536
+
537
+ No. Aspect-oriented programming and lisp-style method advice are more ways of adding "spooky action at a distance", where the code being advised may have no idea that it is having foreign logic attached to it. As well as no control over *where* that foreign logic is applied.
538
+
539
+ In Brainguy, by contrast, objects are explicitly subscribed-to, and events are explicitly emitted.
540
+
541
+ *Is this a library for "Reactive Programming"?*
542
+
543
+ Not in and of itself. It could potentially serve as the foundation for such a library though.
544
+
545
+ *Is this a library for creating "hooks"?*
546
+
547
+ Sort of. Observers do let you "hook" arbitrary handlers to events in other objects. However, this library is not aimed at enabling you to create hooks that *modify* the behavior of other methods. It's primarily intended to allow objects to be *notified* of significant events, without interfering in the processing of the object sending out the notifications.
548
+
549
+ *Is this an asynchronous messaging or reactor system?*
550
+
551
+ No. Brainguy events are processed synchronously have no awareness of concurrency.
552
+
553
+ ## Installation
554
+
555
+ Add this line to your application's Gemfile:
556
+
557
+ ```ruby
558
+ gem 'brainguy'
559
+ ```
560
+
561
+ And then execute:
562
+
563
+ $ bundle
564
+
565
+ Or install it yourself as:
566
+
567
+ $ gem install brainguy
568
+
569
+ ## Usage
570
+
571
+ Coming soon!
572
+
573
+ ## Contributing
574
+
575
+ 1. Fork it ( https://github.com/avdi/brainguy/fork )
576
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
577
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
578
+ 4. Push to the branch (`git push origin my-new-feature`)
579
+ 5. Create a new Pull Request