wisper.cr v1.0.5
Wisper
Minimalistic library to help decouple business concernes using a Pub-Sub approach.
Inspired by Ruby's excelent library Wisper.
- Subscriptions are just callbacks executed when the particular event is emitted
- Local and global subscriptions
- Synchronous and asynchronous subscriptions
- Logging
Installation
-
Add the dependency to your
shard.yml
:dependencies: wisper: github: gmartsenkov/wisper
-
Run
shards install
Usage
To use Wisper simply include it in your class. Since crystal is strictly typed language we'll have to define the possible events that can be broadcasted using the event
macro which just creates a simple class with the defined properties.
class User::Create
include Wisper::Publisher
event Success, name : String, age : Int32
event Failure, reason : String
end
Once events are defined we can broadcast them using #broadcast
-
broadcast(Success.new("Jon", 20))
To subscribe to these events the #on
can be used -
service = User::Create.new
service.on(User::Create::Success) do |success|
puts success.name
end
Async subscriptions, which runs the block in a fiber -
service = User::Create.new
service.on(User::Create::Success, async: true) do |success|
puts success.name
end
Chaining #on
is also possible -
User::Create.new
.on(User::Create::Success) {|success| puts success.name }
.on(User::Create::Failure) {|failure| puts failure.reason }
Sometimes it's usefull to define global subscriptions, for example every time when a new user is successfuly created we want to send out an email -
class Emails
User::Create::GlobalListeners.listen(User::Create::Success, ->welcome_email(User::Create::Success))
def self.welcome_email(success : User::Create::Success)
# Send email logic
end
Also can be run in asynchronously -
User::Create::GlobalListeners.listen(User::Create::Success, ->welcome_email(User::Create::Success) async: true)
NOTE: The local subscription callbacks are executed before the global ones.
Full example -
class User::Create
include Wisper::Publisher
event Success, name : String, age : Int32
event Failure, reason : String
def initialize(@age : Int32)
end
def call
return failure if @age < 18
broadcast(Success.new("Jon", @age))
end
private def failure
broadcast(Failure.new(reason: "Underaged"))
end
end
User::Create.new(age: 18)
.on(User::Create::Success) { |success| puts success.name }
.on(User::Create::Failure) { |failure| puts failure.reason }
.call
Global subscriptions
A global subscription is just a Proc
that is executed when any event is broadcasted from any class or instance. This is usefull for logging, building usage statistics, etc...
Example of how the default wisper logger is implement using a global subscription
logger = ->(event : Wisper::EventTypes) do
attributes = event.attributes
message = "Published - #{event.class.name}"
message += " - #{attributes}" unless attributes.empty?
Log.for("Wisper").info { message }
end
Wisper.listen(logger)
Temporary subscriptions
This is like a global subscription but is only active until the block executes. The test helper capture_events
uses a temporary subscription to capture and return broadcasted events
def capture_events(&block)
events = Array(Wisper::EventTypes).new
handler = ->(event : Wisper::EventTypes) do
events << event
end
Wisper.listen(handler) do
yield
end
events
end
puts capture_events { User::Create.new(age: 10).call }
# [#<User::Create::Failure:0x1022d9b20 @reason="Underaged">]
Logging
We can use a global listener to log every broadcasted event. There is a default logger that can be used like this ->
require "wisper"
Wisper.listen(->Wisper.default_logger(Wisper::EventTypes))
Testing
There is a helper method in wisper/test
called #capture_events
that takes a block and returns the broadcasted events in that block. Example on testing with Specter
require "wisper/test"
it "calls the correct subscription" do
service = User::Create.new(17)
service.on(User::Create::Failure) do |failure|
expect(failure.reason).to eq "teast"
end
events = capture_events { service.call }
expect(events).to have User::Create::Failure
end
Spectator
Handy spectator matchers to easily test an event has been broadcasted - https://github.com/gmartsenkov/wisper-spectator
require "wisper-spectator"
Spectator.describe "User::Create" do
subject { User::Create.new(15) }
describe "#broadcast" do
it "works with a passed event class" do
subject.on(User::Create::Failure) do |failure|
expect(failure.reason).to eq "Some reason"
end
expect { subject.call }.to broadcast(User::Create::Failure)
end
end
end
Contributing
- Fork it (https://github.com/gmartsenkov/wisper/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Contributors
- Georgi Martsenkov - creator and maintainer
wisper.cr
- 1
- 0
- 0
- 1
- 1
- about 2 years ago
- August 30, 2022
MIT License
Fri, 08 Nov 2024 01:38:36 GMT