README.md in wisper-1.0.0 vs README.md in wisper-1.0.1

- old
+ new

@@ -1,14 +1,14 @@ # Wisper Simple pub/sub for Ruby objects [![Code Climate](https://codeclimate.com/github/krisleech/wisper.png)](https://codeclimate.com/github/krisleech/wisper) -[![Build Status](https://travis-ci.org/krisleech/wisper.png)](https://travis-ci.org/krisleech/wisper) +[![Build Status](https://travis-ci.org/krisleech/wisper.png?branch=master)](https://travis-ci.org/krisleech/wisper) While this is not dependent on Rails in any way it was extracted from a Rails -project and is used as an alternative to ActiveRecord callbacks and Observers. +project and can used as an alternative to ActiveRecord callbacks and Observers. The problem with callbacks and Observers is that they always happen. How many times have you wanted to do `User.create` without firing off a welcome email? It is also super useful for integrating web socket notifications, statistics @@ -17,11 +17,13 @@ ## Installation Add this line to your application's Gemfile: - gem 'wisper' +```ruby +gem 'wisper', '~>1.0.0' +``` ## Usage Any class with the Wisper module included can broadcast events to subscribed listeners. Listeners are added, at runtime, to the publishing object. @@ -31,10 +33,11 @@ ```ruby class MyPublisher include Wisper def do_something + # ... publish(:done_something, self) end end ``` @@ -47,29 +50,46 @@ ### Subscribing #### Listeners -The listener is subscribed to all events it responds to. +Any object can be a listener and by default they are only subscribed to events +they can respond to. ```ruby -listener = Object.new # any object my_publisher = MyPublisher.new -my_publisher.subscribe(listener) +my_publisher.subscribe(MyListener.new) ``` #### Blocks -The block is subscribed to a single event. +Blocks are subscribed to single events. ```ruby my_publisher = MyPublisher.new my_publisher.on(:done_something) do |publisher| # ... end ``` +### Asynchronous Publishing (Experimental) + +There is support for publishing events asynchronously by passing the `async` +option. + +```ruby +my_publisher.add_subscriber(MySubscriber.new, :async => true) +``` + +This leans on Celluloid, which must be included in your Gemfile. + +The listener is transparently turned in to a Celluloid Actor. + +Please refer to [Celluloid](https://github.com/celluloid/celluloid/wiki) +for more information, particually the +[Gotchas](https://github.com/celluloid/celluloid/wiki/Gotchas). + ### ActiveRecord ```ruby class Post < ActiveRecord::Base include Wisper @@ -101,38 +121,42 @@ @post.create end end ``` -### Service/Use case object +### Service/Use Case/Command objects -The downside to publishing directly from ActiveRecord models is that an event -can get fired and then rolled back if a transaction fails. +A Service object is useful when an operation is complex, interacts with more +than one model, accesses an external API or would burden a model with too much +responsibility. -Since I am trying to make my models dumb I tend to use a separate service -object which contains all the logic and wraps it all in a transaction. - -The follow is contrived, but you can imagine it doing more than just updating a -record, maybe sending an email or updating other records. - ```ruby -class CreateThing +class PlayerJoiningTeam include Wisper - def execute(attributes) - thing = Thing.new(attributes) + def execute(player, team) + membership = Membership.new(player, team) - if thing.valid? - ActiveRecord::Base.transaction do - thing.save - # ... - end - publish(:create_thing_successful, thing) + if membership.valid? + membership.save! + email_player(player, team) + assign_first_mission(player, team) + publish(:player_joining_team_successful, player, team) else - publish(:create_thing_failed, thing) + publish(:player_joining_team_failed, player, team) end end + + private + + def email_player(player, team) + # ... + end + + def assign_first_mission(player, team) + # ... + end end ``` ### Example listeners @@ -155,17 +179,44 @@ class StatisticsListener def create_thing_successful(thing) # ... end end + +class CacheListener + def create_thing_successful(thing) + # ... + end +end + +class IndexingListener + def create_thing_successful(thing) + # ... + end +end ``` +## Global listeners + +If you become tired of adding the same listeners to _every_ publisher you can +add global listeners. They receive all published events which they can respond +to. + +However it means that when looking at the code it will not be obvious that the +global listeners are being executed in additional to the regular listeners. + +```ruby +Wisper::GlobalListeners.add_listener(MyListener.new) +``` + +In a Rails app you might want to add your global listeners in an initalizer. + ## Subscribing to selected events -By default a listener will get notified of all events it responds to. You can -limit which events a listener is notified of by passing an event or array of -events to `:on`. +By default a listener will get notified of all events it can respond to. You +can limit which events a listener is notified of by passing an event or array +of events to `:on`. ```ruby post_creater.subscribe(PusherListener.new, :on => :create_post_successful) ``` @@ -204,9 +255,60 @@ ```ruby post.on(:success) { |post| redirect_to post } .on(:failure) { |post| render :action => :edit, :locals => :post => post } ``` + +## RSpec + +Wisper comes with a method for stubbing event publishers so that you can create isolation tests +that only care about reacting to events. + +Given this piece of code: + +```ruby +class CodeThatReactsToEvents + def do_something + publisher = MyPublisher.new + publisher.on(:some_event) do |variable| + return "Hello with #{variable}!" + end + publisher.execute + end +end +``` + +You can test it like this: + +```ruby +require 'wisper/rspec/stub_wisper_publisher' + +describe CodeThatReactsToEvents do + context "on some_event" do + before do + stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo") + end + + it "renders" do + response = CodeThatReactsToEvents.new.do_something + response.should == "Hello with foo!" + end + end +end +``` + +This becomes important when testing, for example, Rails controllers in +isolation from the business logic. This technique is used at the controller +layer to isolate testing the controller from testing the encapsulated business +logic. + +You can use any number of args to pass to the event: + +```ruby +stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo1", "foo2", ...) +``` + +See `spec/lib/rspec_extensions_spec.rb` for a runnable example. ## Compatibility Tested with 1.9.x on MRI, JRuby and Rubinius. See the [build status](https://travis-ci.org/krisleech/wisper) for details.