octiron
Octiron is an event bus with the ability to magically transform events.
Events octiron responds to can be any classes or Hash prototypes. Using transmogrifiers, events can be turned into other kinds of events transparently.
Usage
So what does this all mean?
Fundamentals
First of all, the gem contains a publish/subscribe event bus, which easily allows you to subscribe handlers to events of a particular class:
require 'octiron'
class MyEvent
# ...
end
include Octiron::World
on_event(MyEvent) do |event|
# event.is_a?(MyEvent) == true
# do something with the event
end
You can subscribe as many handlers to an event class as you want.
Next, you can transmogrify objects - typically events. Similar to suscribing event handlers, you can register transmogrifiers for a particular transmogrification.
class AnotherEvent
end
on_transmogrify(MyEvent).to AnotherEvent do |my_event|
# guaranteed: my_event.is_a?(MyEvent) == true
AnotherEvent.new
end
As long as the transmogrifier returns an object of the AnotherEvent
type,
all is well. Otherwise, an exception is raised.
Putting this together with the hitherto unmentioned #publish
method, you can
easily build processing pipelines.
on_transmogrify(MyEvent).to AnotherEvent do |my_event|
# do something more meaningful
AnotherEvent.new
end
on_event(MyEvent) do |event|
publish transmogrify(event).to AnotherEvent
end
on_event(AnotherEvent) do |event|
# We'll end up here with the transmogrified event
end
publish MyEvent.new
Advanced Usage
There are some more advanced topics that might be useful to understand.
Automatic Transmogrification
When you write a lot of transmogrifiers, chances are your event handlers all look roughly the same:
on_event(SourceEvent) do |event|
begin
new_event = transmogrify(event).to AnotherEvent
publish(new_event)
rescue RuntimeError
end
end
This event handler pattern tries to transmogrify the source event to another type, and on success will publish the result. On failure, it just wants to swallow the event.
This pattern is supported with the #autotransmogrify
function:
autotransmogrify(SourceEvent) do |event|
if not some_condition
next
end
AnotherEvent.new
end
Your transmogrifier is installed as previously, and additionally an event handler is registered that follows the pattern above. If the transmogrifier returns no new event, that is silently accepted.
You can use #autotransmogrify
and still raise errors, of course:
autotransmogrify(SourceEvent, raise_on_nil: true) do |_|
# do nothing
end
publish(SourceEvent.new) # will raise RuntimeError
Handler Classes
The term "Handler Class" should not be confused with Ruby classes implementing handlers. Instead, a class is a grouping "type".
There may be situations where you want your handlers to be executed in a particular order. That is especially the case when one handler relies on a different handler having been executed earlier. For those instances, you can sort your handlers into classes as you subscribe them:
on_event(MyEvent, nil, SOME_CLASS) do |event|
# ...
end
on_event(MyEvent, nil, ANOTHER_CLASS) do |event|
# ...
end
In the above example, the two handlers are subscribed with the SOME_CLASS
and
ANOTHER_CLASS
constants as handler classes respectively. These parameters must
be sortable values: if ANOTHER_CLASS
's value is sorted before SOME_CLASS
,
then the second handler is executed before the first, e.g. in this instance:
ANOTHER_CLASS = 0
SOME_CLASS = 1
Note that we're passing a nil
object as the second parameter every time. This
is because we're subscribing a block handler. For an object handler, the code
would have to be the following:
on_event(MyEvent, second_handler, SOME_CLASS)
on_event(MyEvent, first_handler, ANOTHER_CLASS)
Since handlers don't create new events, but pass the event from one handler to another, ordering handlers in classes allows you to modify an event before it reaches another handler.
Singletons vs. API
The octiron gem exposes a number of simple wrapper functions we've used so far to hide the API complexity a little. These wrapper functions make use of a singleton event bus instance, and a singleton transmogrifier registry:
#on_event
delegatesOctiron::Events::Bus#subscribe
#publish
delegatesOctiron::Events::Bus#publish
#on_transmogrify
delegates toOctiron::Transmogrifiers::Registry#register
#transmogrify
delegates toOctiron::Transmogrifiers::Registry#transmogrify
You can just as well use these underlying API functions with multiple instances of the event bus or the transmogrifier registry.
Hash Prototypes
The octiron gem implements something of a prototyping mechanic for using Hashes as events, but also as sources and targets of transmogrification. If the event bus would register handlers for the Hash class, all Hashes would trigger the same handlers (and similar for the transmogrifier registry).
Instead, where you would normally provide classes (e.g. #on_event
), you can
specify a Hash that acts as a prototype. Then, where you would normally use
instances (e.g. #publish
), you can provide a Hash again. If that published
Hash matches the prototype Hash, the handler associated with the prototype is
triggered.
So how does this matching work?
- If a prototype specifies a key without value (i.e. a nil value), the published instance must contain the key, but the value or value type is ignored.
- If a prototype specifies a key with a value, the published instance must contain the key, and it's value must match the respective value in the prototype.
proto1 = {
a: nil,
}
proto2 = {
b: 42,
}
published1 = {
a: 'foo',
}
# published1 matches proto1, but not proto2
published2 = {
b: 42,
}
# published2 matches proto2, but not proto1
Hash prototyping is supported both for subscribing event handlers, and registering transmogrifiers, allowing for something of the following:
on_transmogrify(proto1).to proto2 do |event|
x = event.dup
x.delete(:a)
x[:b] = 42
x
end
on_event(proto1) do |event|
publish transmogrify(event).to proto2
end
on_event(proto2) do |event|
# we'll end up here
end
publish published1
Note that you can produce closed loops by publishing events from within event
handlers. In the above example, if the transmogrifier did not delete the :a
key from the newly created event, it would still match the proto1
prototype,
which would trigger that handler and the transmogrifier again and again.