wisper-2.0.1/0000755000175000017500000000000013620176654012675 5ustar gabstergabsterwisper-2.0.1/wisper.gemspec0000644000175000017500000000234013620176654015552 0ustar gabstergabster# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'wisper/version' Gem::Specification.new do |gem| gem.name = "wisper" gem.version = Wisper::VERSION gem.authors = ["Kris Leech"] gem.email = ["kris.leech@gmail.com"] gem.description = <<-DESC A micro library providing objects with Publish-Subscribe capabilities. Both synchronous (in-process) and asynchronous (out-of-process) subscriptions are supported. Check out the Wiki for articles, guides and examples: https://github.com/krisleech/wisper/wiki DESC gem.summary = "A micro library providing objects with Publish-Subscribe capabilities" gem.homepage = "https://github.com/krisleech/wisper" gem.license = "MIT" signing_key = File.expand_path(ENV['HOME'].to_s + '/.ssh/gem-private_key.pem') if File.exist?(signing_key) gem.signing_key = signing_key gem.cert_chain = ['gem-public_cert.pem'] end gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] end wisper-2.0.1/spec/0000755000175000017500000000000013620176654013627 5ustar gabstergabsterwisper-2.0.1/spec/spec_helper.rb0000644000175000017500000000073613620176654016453 0ustar gabstergabsterrequire 'coveralls' Coveralls.wear! require 'wisper' RSpec.configure do |config| config.run_all_when_everything_filtered = true config.filter_run :focus config.order = 'random' config.after(:each) { Wisper::GlobalListeners.clear } config.expect_with :rspec do |c| c.syntax = :expect end config.mock_with :rspec do |c| c.syntax = :expect end end # returns an anonymous wispered class def publisher_class Class.new { include Wisper::Publisher } end wisper-2.0.1/spec/lib/0000755000175000017500000000000013620176654014375 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper_spec.rb0000644000175000017500000000525613620176654017255 0ustar gabstergabsterdescribe Wisper do describe '.subscribe' do context 'when given block' do it 'subscribes listeners to all events for duration of the block' do publisher = publisher_class.new listener = double('listener') expect(listener).to receive(:first_event) expect(listener).not_to receive(:second_event) Wisper.subscribe(listener) do publisher.send(:broadcast, 'first_event') end publisher.send(:broadcast, 'second_event') end end context 'when no block given' do it 'subscribes listener to all events' do listener = double('listener') Wisper.subscribe(listener) expect(Wisper::GlobalListeners.listeners).to eq [listener] end it 'subscribes listeners to all events' do listener_1 = double('listener') listener_2 = double('listener') Wisper.subscribe(listener_1, listener_2) expect(Wisper::GlobalListeners.listeners).to include listener_1, listener_2 end end end describe '.unsubscribe' do it 'removes listener from list of listeners' do listener = double('listener') Wisper.subscribe(listener) expect(Wisper::GlobalListeners.listeners).to eq [listener] Wisper.unsubscribe(listener) expect(Wisper::GlobalListeners.listeners).to eq [] end it 'removes listeners from list of listeners' do listener_1 = double('listener') listener_2 = double('listener') Wisper.subscribe(listener_1, listener_2) expect(Wisper::GlobalListeners.listeners).to include listener_1, listener_2 Wisper.unsubscribe(listener_1, listener_2) expect(Wisper::GlobalListeners.listeners).to eq [] end end describe '.publisher' do it 'returns the Publisher module' do expect(Wisper.publisher).to eq Wisper::Publisher end end describe '.clear' do before { Wisper.subscribe(double) } it 'clears all global listeners' do Wisper.clear expect(Wisper::GlobalListeners.listeners).to be_empty end end describe '.configuration' do it 'returns configuration object' do expect(Wisper.configuration).to be_an_instance_of(Wisper::Configuration) end it 'is memorized' do expect(Wisper.configuration).to eq Wisper.configuration end end describe '.configure' do it 'passes configuration to given block' do Wisper.configure do |config| expect(config).to be_an_instance_of(Wisper::Configuration) end end end describe '.setup' do it 'sets a default broadcaster' do expect(Wisper.configuration.broadcasters[:default]).to be_instance_of(Wisper::Broadcasters::SendBroadcaster) end end end wisper-2.0.1/spec/lib/wisper/0000755000175000017500000000000013620176654015706 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper/value_objects/0000755000175000017500000000000013620176654020533 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper/value_objects/prefix_spec.rb0000644000175000017500000000205613620176654023372 0ustar gabstergabsterdescribe Wisper::ValueObjects::Prefix do it 'is a string' do expect(subject).to be_kind_of String end describe '.new' do context 'without arguments' do subject { described_class.new } it { is_expected.to eq '' } end context 'nil' do subject { described_class.new nil } it { is_expected.to eq '' } end context 'true' do subject { described_class.new true } it { is_expected.to eq 'on_' } end context '"foo"' do subject { described_class.new 'foo' } it { is_expected.to eq 'foo_' } end end describe '.default=' do after { described_class.default = nil } context 'nil' do it "doesn't change default prefix" do expect { described_class.default = nil } .not_to change { described_class.new true }.from('on_') end end context '"foo"' do it 'changes default prefix' do expect { described_class.default = 'foo' } .to change { described_class.new true }.from('on_').to('foo_') end end end end wisper-2.0.1/spec/lib/wisper/value_objects/events_spec.rb0000644000175000017500000000506213620176654023401 0ustar gabstergabsterdescribe Wisper::ValueObjects::Events do context 'nil' do subject { described_class.new nil } describe '#include?' do it 'returns true' do expect(subject.include? 'foo').to be_truthy expect(subject.include? :bar).to be_truthy end end end context '"foo"' do let(:foo) { Class.new(String).new 'foo' } subject { described_class.new foo } describe '#include?' do it 'returns true for "foo"' do expect(subject.include? 'foo').to be_truthy end it 'returns true for :foo' do expect(subject.include? :foo).to be_truthy end it 'returns false otherwise' do expect(subject.include? 'bar').to be_falsey expect(subject.include? :bar).to be_falsey end end end context ':foo' do subject { described_class.new :foo } describe '#include?' do it 'returns true for "foo"' do expect(subject.include? 'foo').to be_truthy end it 'returns true for :foo' do expect(subject.include? :foo).to be_truthy end it 'returns false otherwise' do expect(subject.include? 'bar').to be_falsey expect(subject.include? :bar).to be_falsey end end end context '[:foo, "bar"]' do subject { described_class.new [:foo, 'bar'] } describe '#include?' do it 'returns true for "foo"' do expect(subject.include? 'foo').to be_truthy end it 'returns true for :foo' do expect(subject.include? :foo).to be_truthy end it 'returns true for "bar"' do expect(subject.include? 'bar').to be_truthy end it 'returns true for :bar' do expect(subject.include? :bar).to be_truthy end it 'returns false otherwise' do expect(subject.include? 'baz').to be_falsey expect(subject.include? :baz).to be_falsey end end end context 'by /foo/' do subject { described_class.new(/foo/) } describe '#include?' do it 'returns true for "foo"' do expect(subject.include? 'foo').to be_truthy end it 'returns true for :foo' do expect(subject.include? :foo).to be_truthy end it 'returns false otherwise' do expect(subject.include? 'bar').to be_falsey expect(subject.include? :bar).to be_falsey end end end context 'another class' do subject { described_class.new Object.new } describe '#include?' do it 'raises ArgumentError' do expect { subject.include? 'foo' }.to raise_error(ArgumentError) end end end end wisper-2.0.1/spec/lib/wisper/registrations/0000755000175000017500000000000013620176654020603 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper/registrations/object_spec.rb0000644000175000017500000000101613620176654023406 0ustar gabstergabsterdescribe Wisper::ObjectRegistration do describe 'broadcaster' do it 'defaults to SendBroadcaster' do subject = Wisper::ObjectRegistration.new(double('listener'), {}) expect(subject.broadcaster).to be_instance_of(Wisper::Broadcasters::SendBroadcaster) end it 'default is lazily evaluated' do expect(Wisper::Broadcasters::SendBroadcaster).to_not receive :new Wisper::ObjectRegistration.new(double('listener'), broadcaster: double('DifferentBroadcaster').as_null_object) end end end wisper-2.0.1/spec/lib/wisper/publisher_spec.rb0000644000175000017500000002376613620176654021260 0ustar gabstergabsterdescribe Wisper::Publisher do let(:listener) { double('listener') } let(:publisher) { publisher_class.new } describe '.subscribe' do it 'subscribes given listener to all published events' do expect(listener).to receive(:this_happened) expect(listener).to receive(:so_did_this) publisher.subscribe(listener) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'so_did_this') end describe ':on argument' do before do allow(listener).to receive(:something_a_happened) allow(listener).to receive(:and_this) allow(listener).to receive(:so_did_this) end describe 'given a string' do it 'subscribes listener to an event' do expect(listener).to receive(:this_happened) expect(listener).not_to receive(:so_did_this) publisher.subscribe(listener, on: 'this_happened') publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'so_did_this') end end describe 'given a symbol' do it 'subscribes listener to an event' do expect(listener).to receive(:this_happened) expect(listener).not_to receive(:so_did_this) publisher.subscribe(listener, on: :this_happened) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'so_did_this') end end describe 'given an array' do it 'subscribes listener to events' do expect(listener).to receive(:this_happened) expect(listener).to receive(:and_this) expect(listener).not_to receive(:so_did_this) publisher.subscribe(listener, on: ['this_happened', 'and_this']) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'so_did_this') publisher.send(:broadcast, 'and_this') end end describe 'given a regex' do it 'subscribes listener to matching events' do expect(listener).to receive(:something_a_happened) expect(listener).not_to receive(:so_did_this) publisher.subscribe(listener, on: /something_._happened/) publisher.send(:broadcast, 'something_a_happened') publisher.send(:broadcast, 'so_did_this') end end describe 'given an unsupported argument' do it 'raises an error' do publisher.subscribe(listener, on: Object.new) expect { publisher.send(:broadcast, 'something_a_happened') }.to raise_error(ArgumentError) end end end describe ':with argument' do it 'sets method to call listener with on event' do expect(listener).to receive(:different_method).twice publisher.subscribe(listener, :with => :different_method) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'so_did_this') end end describe ':prefix argument' do it 'prefixes broadcast events with given symbol' do expect(listener).to receive(:after_it_happened) expect(listener).not_to receive(:it_happened) publisher.subscribe(listener, :prefix => :after) publisher.send(:broadcast, 'it_happened') end it 'prefixes broadcast events with "on" when given true' do expect(listener).to receive(:on_it_happened) expect(listener).not_to receive(:it_happened) publisher.subscribe(listener, :prefix => true) publisher.send(:broadcast, 'it_happened') end end describe ':scope argument' do let(:listener_1) { double('Listener') } let(:listener_2) { double('Listener') } it 'scopes listener to given class' do expect(listener_1).to receive(:it_happended) expect(listener_2).not_to receive(:it_happended) publisher.subscribe(listener_1, :scope => publisher.class) publisher.subscribe(listener_2, :scope => Class.new) publisher.send(:broadcast, 'it_happended') end it 'scopes listener to given class string' do expect(listener_1).to receive(:it_happended) expect(listener_2).not_to receive(:it_happended) publisher.subscribe(listener_1, :scope => publisher.class.to_s) publisher.subscribe(listener_2, :scope => Class.new.to_s) publisher.send(:broadcast, 'it_happended') end it 'includes all subclasses of given class' do publisher_super_klass = publisher_class publisher_sub_klass = Class.new(publisher_super_klass) listener = double('Listener') expect(listener).to receive(:it_happended).once publisher = publisher_sub_klass.new publisher.subscribe(listener, :scope => publisher_super_klass) publisher.send(:broadcast, 'it_happended') end end describe ':broadcaster argument'do let(:broadcaster) { double('broadcaster') } let(:listener) { double('listener') } let(:event_name) { 'it_happened' } before do Wisper.configuration.broadcasters.clear allow(listener).to receive(event_name) allow(broadcaster).to receive(:broadcast) end after { Wisper.setup } # restore default configuration it 'given an object which responds_to broadcast it uses object' do publisher.subscribe(listener, broadcaster: broadcaster) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end it 'given a key it uses a configured broadcaster' do Wisper.configure { |c| c.broadcaster(:foobar, broadcaster) } publisher.subscribe(listener, broadcaster: :foobar) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end it 'given an unknown key it raises error' do expect { publisher.subscribe(listener, broadcaster: :foobar) }.to raise_error(KeyError, /broadcaster not found/) end it 'given nothing it uses the default broadcaster' do Wisper.configure { |c| c.broadcaster(:default, broadcaster) } publisher.subscribe(listener) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end describe 'async alias' do it 'given an object which responds_to broadcast it uses object' do publisher.subscribe(listener, async: broadcaster) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end it 'given true it uses configured async broadcaster' do Wisper.configure { |c| c.broadcaster(:async, broadcaster) } publisher.subscribe(listener, async: true) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end it 'given false it uses configured default broadcaster' do Wisper.configure { |c| c.broadcaster(:default, broadcaster) } publisher.subscribe(listener, async: false) expect(broadcaster).to receive('broadcast') publisher.send(:broadcast, event_name) end end end it 'returns publisher so methods can be chained' do expect(publisher.subscribe(listener, :on => 'so_did_this')).to \ eq publisher end it 'is aliased to .subscribe' do expect(publisher).to respond_to(:subscribe) end it 'raises a helpful error if trying to pass a block' do invalid = ->{ publisher.subscribe(:success) do puts end } expect{ invalid.call }.to raise_error(ArgumentError) end end describe '.on' do it 'returns publisher so methods can be chained' do expect(publisher.on('this_thing_happened') {}).to eq publisher end it 'raise an error if no events given' do expect { publisher.on() {} }.to raise_error(ArgumentError) end it 'raises an error of no block given' do expect { publisher.on(:something) }.to raise_error(ArgumentError) end it 'returns publisher so methods can be chained' do expect(publisher.on(:foo) {}).to eq publisher end end describe '.broadcast' do it 'does not publish events which cannot be responded to' do expect(listener).not_to receive(:so_did_this) allow(listener).to receive(:respond_to?).and_return(false) publisher.subscribe(listener, :on => 'so_did_this') publisher.send(:broadcast, 'so_did_this') end describe ':event argument' do it 'is indifferent to string and symbol' do expect(listener).to receive(:this_happened).twice publisher.subscribe(listener) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, :this_happened) end it 'is indifferent to dasherized and underscored strings' do expect(listener).to receive(:this_happened).twice publisher.subscribe(listener) publisher.send(:broadcast, 'this_happened') publisher.send(:broadcast, 'this-happened') end end it 'returns publisher' do expect(publisher.send(:broadcast, :foo)).to eq publisher end it 'is not public' do expect(publisher).not_to respond_to(:broadcast) end it 'is alised as .publish' do expect(publisher.method(:broadcast)).to eq publisher.method(:publish) end end describe '.listeners' do it 'returns an immutable collection' do expect(publisher.listeners).to be_frozen expect { publisher.listeners << listener }.to raise_error(RuntimeError) end it 'returns local listeners' do publisher.subscribe(listener) expect(publisher.listeners).to eq [listener] expect(publisher.listeners.size).to eq 1 end end describe '#subscribe' do let(:publisher_klass_1) { publisher_class } let(:publisher_klass_2) { publisher_class } it 'subscribes listener to all instances of publisher' do publisher_klass_1.subscribe(listener) expect(listener).to receive(:it_happened).once publisher_klass_1.new.send(:broadcast, 'it_happened') publisher_klass_2.new.send(:broadcast, 'it_happened') end end end wisper-2.0.1/spec/lib/wisper/configuration_spec.rb0000644000175000017500000000201013620176654022105 0ustar gabstergabstermodule Wisper describe Configuration do describe 'broadcasters' do let(:broadcaster) { double } let(:key) { :default } it '#broadcasters returns empty collection' do expect(subject.broadcasters).to be_empty end describe '#broadcaster' do it 'adds given broadcaster' do subject.broadcaster(key, broadcaster) expect(subject.broadcasters).to include key expect(subject.broadcasters[key]).to eql broadcaster end it 'returns the configuration' do expect(subject.broadcaster(key, broadcaster)).to eq subject end end end describe '#default_prefix=' do let(:prefix_class) { ValueObjects::Prefix } let(:default_value) { double } before { allow(prefix_class).to receive(:default=) } it 'sets the default value for prefixes' do expect(prefix_class).to receive(:default=).with(default_value) subject.default_prefix = default_value end end end end wisper-2.0.1/spec/lib/wisper/configuration/0000755000175000017500000000000013620176654020555 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper/configuration/broadcasters_spec.rb0000644000175000017500000000035713620176654024575 0ustar gabstergabstermodule Wisper describe Configuration::Broadcasters do describe 'broadcasters' do describe '#to_h' do it 'returns a Hash' do expect(subject.to_h).to be_instance_of(Hash) end end end end end wisper-2.0.1/spec/lib/wisper/broadcasters/0000755000175000017500000000000013620176654020362 5ustar gabstergabsterwisper-2.0.1/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb0000644000175000017500000000140713620176654025225 0ustar gabstergabstermodule Wisper module Broadcasters describe SendBroadcaster do let(:listener) { double('listener') } let(:event) { 'thing_created' } describe '#broadcast' do context 'without arguments' do let(:args) { [] } it 'sends event to listener without any arguments' do expect(listener).to receive(event).with(no_args()) subject.broadcast(listener, anything, event, args) end end context 'with arguments' do let(:args) { [1,2,3] } it 'sends event to listener with arguments' do expect(listener).to receive(event).with(*args) subject.broadcast(listener, anything, event, args) end end end end end end wisper-2.0.1/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb0000644000175000017500000000671013620176654025555 0ustar gabstergabstermodule Wisper module Broadcasters describe LoggerBroadcaster do describe 'integration tests:' do let(:publisher) { publisher_class.new } let(:listener) { double } let(:logger) { double.as_null_object } it 'broadcasts the event to the listener' do publisher.subscribe(listener, :broadcaster => LoggerBroadcaster.new(logger, Wisper::Broadcasters::SendBroadcaster.new)) expect(listener).to receive(:it_happened).with(1, 2) publisher.send(:broadcast, :it_happened, 1, 2) end end describe 'unit tests:' do let(:publisher) { classy_double('Publisher', id: 1) } let(:listener) { classy_double('Listener', id: 2) } let(:logger) { double('Logger').as_null_object } let(:broadcaster) { double('Broadcaster').as_null_object } let(:event) { 'thing_created' } subject { LoggerBroadcaster.new(logger, broadcaster) } describe '#broadcast' do context 'without arguments' do let(:args) { [] } it 'logs published event' do expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with no arguments') subject.broadcast(listener, publisher, event, args) end it 'delegates broadcast to a given broadcaster' do expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args) subject.broadcast(listener, publisher, event, args) end end context 'with arguments' do let(:args) { [arg_double(id: 3), arg_double(id: 4)] } it 'logs published event and arguments' do expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with Argument#3, Argument#4') subject.broadcast(listener, publisher, event, args) end it 'delegates broadcast to a given broadcaster' do expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args) subject.broadcast(listener, publisher, event, args) end context 'when argument is a hash' do let(:args) { [hash] } let(:hash) { {key: 'value'} } it 'logs published event and arguments' do expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with Hash##{hash.object_id}: #{hash.inspect}") subject.broadcast(listener, publisher, event, args) end end context 'when argument is an integer' do let(:args) { [number] } let(:number) { 10 } it 'logs published event and arguments' do expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with #{number.class.name}##{number.object_id}: 10") subject.broadcast(listener, publisher, event, args) end end end end # provides a way to specify `double.class.name` easily def classy_double(klass, options) double(klass, options.merge(class: double_class(klass))) end def arg_double(options) classy_double('Argument', options) end def double_class(name) double(name: name) end end end end end wisper-2.0.1/spec/lib/temporary_global_listeners_spec.rb0000644000175000017500000000636313620176654023376 0ustar gabstergabsterdescribe Wisper::TemporaryListeners do let(:listener_1) { double('listener', :to_a => nil) } # [1] let(:listener_2) { double('listener', :to_a => nil) } let(:publisher) { publisher_class.new } describe '.subscribe' do it 'globally subscribes listener for duration of given block' do expect(listener_1).to receive(:success) expect(listener_1).to_not receive(:failure) Wisper::TemporaryListeners.subscribe(listener_1) do publisher.instance_eval { broadcast(:success) } end publisher.instance_eval { broadcast(:failure) } end it 'globally subscribes listeners for duration of given block' do expect(listener_1).to receive(:success) expect(listener_1).to_not receive(:failure) expect(listener_2).to receive(:success) expect(listener_2).to_not receive(:failure) Wisper::TemporaryListeners.subscribe(listener_1, listener_2) do publisher.instance_eval { broadcast(:success) } end publisher.instance_eval { broadcast(:failure) } end it 'globally subscribes listeners for duration of nested block' do expect(listener_1).to receive(:success) expect(listener_1).to receive(:failure) expect(listener_2).to receive(:success) expect(listener_2).to_not receive(:failure) Wisper::TemporaryListeners.subscribe(listener_1) do Wisper::TemporaryListeners.subscribe(listener_2) do publisher.instance_eval { broadcast(:success) } end publisher.instance_eval { broadcast(:failure) } end end it 'clears registrations for the block which exits' do # listener_1 is subscribed twice hence it's supposed to get the message twice expect(listener_1).to receive(:success).twice expect(listener_1).to receive(:failure) expect(listener_1).to_not receive(:ignored) expect(listener_2).to receive(:success) expect(listener_2).to_not receive(:failure) expect(listener_2).to_not receive(:ignored) Wisper::TemporaryListeners.subscribe(listener_1) do Wisper::TemporaryListeners.subscribe(listener_1, listener_2) do publisher.instance_eval { broadcast(:success) } end publisher.instance_eval { broadcast(:failure) } end publisher.instance_eval { broadcast(:ignored) } end it 'is thread safe' do num_threads = 20 (1..num_threads).to_a.map do Thread.new do Wisper::TemporaryListeners.registrations << Object.new expect(Wisper::TemporaryListeners.registrations.size).to eq 1 end end.each(&:join) expect(Wisper::TemporaryListeners.registrations).to be_empty end it 'clears registrations when an exception occurs' do MyError = Class.new(StandardError) begin Wisper::TemporaryListeners.subscribe(listener_1) do raise MyError end rescue MyError end expect(Wisper::TemporaryListeners.registrations).to be_empty end it 'returns self' do expect(Wisper::TemporaryListeners.subscribe {}).to be_an_instance_of(Wisper::TemporaryListeners) end end end # [1] stubbing `to_a` prevents `Double "listener" received unexpected message # :to_a with (no args)` on MRI 1.9.2 when a double is passed to `Array()`. wisper-2.0.1/spec/lib/simple_example_spec.rb0000644000175000017500000000075413620176654020746 0ustar gabstergabsterclass MyPublisher include Wisper::Publisher def do_something # ... broadcast(:bar, self) broadcast(:foo, self) end end describe 'simple publishing' do it 'subscribes listener to events' do listener = double('listener') expect(listener).to receive(:foo).with instance_of MyPublisher expect(listener).to receive(:bar).with instance_of MyPublisher my_publisher = MyPublisher.new my_publisher.subscribe(listener) my_publisher.do_something end end wisper-2.0.1/spec/lib/integration_spec.rb0000644000175000017500000000250013620176654020254 0ustar gabstergabster# Example class MyCommand include Wisper::Publisher def execute(be_successful) if be_successful broadcast('success', 'hello') else broadcast('failure', 'world') end end end describe Wisper do it 'subscribes object to all published events' do listener = double('listener') expect(listener).to receive(:success).with('hello') command = MyCommand.new command.subscribe(listener) command.execute(true) end it 'maps events to different methods' do listener_1 = double('listener') listener_2 = double('listener') expect(listener_1).to receive(:happy_days).with('hello') expect(listener_2).to receive(:sad_days).with('world') command = MyCommand.new command.subscribe(listener_1, :on => :success, :with => :happy_days) command.subscribe(listener_2, :on => :failure, :with => :sad_days) command.execute(true) command.execute(false) end it 'subscribes block can be chained' do insider = double('Insider') expect(insider).to receive(:render).with('success') expect(insider).to receive(:render).with('failure') command = MyCommand.new command.on(:success) { |message| insider.render('success') } .on(:failure) { |message| insider.render('failure') } command.execute(true) command.execute(false) end end wisper-2.0.1/spec/lib/global_listeners_spec.rb0000644000175000017500000000541313620176654021267 0ustar gabstergabsterdescribe Wisper::GlobalListeners do let(:global_listener) { double('listener') } let(:local_listener) { double('listener') } let(:publisher) { publisher_class.new } describe '.subscribe' do it 'adds given listener to every publisher' do Wisper::GlobalListeners.subscribe(global_listener) expect(global_listener).to receive(:it_happened) publisher.send(:broadcast, :it_happened) end it 'works with options' do Wisper::GlobalListeners.subscribe(global_listener, :on => :it_happened, :with => :woot) expect(global_listener).to receive(:woot).once expect(global_listener).not_to receive(:it_happened_again) publisher.send(:broadcast, :it_happened) publisher.send(:broadcast, :it_happened_again) end it 'works along side local listeners' do # global listener Wisper::GlobalListeners.subscribe(global_listener) # local listener publisher.subscribe(local_listener) expect(global_listener).to receive(:it_happened) expect(local_listener).to receive(:it_happened) publisher.send(:broadcast, :it_happened) end it 'can be scoped to classes' do publisher_1 = publisher_class.new publisher_2 = publisher_class.new publisher_3 = publisher_class.new Wisper::GlobalListeners.subscribe(global_listener, :scope => [publisher_1.class, publisher_2.class]) expect(global_listener).to receive(:it_happened_1).once expect(global_listener).to receive(:it_happened_2).once expect(global_listener).not_to receive(:it_happened_3) publisher_1.send(:broadcast, :it_happened_1) publisher_2.send(:broadcast, :it_happened_2) publisher_3.send(:broadcast, :it_happened_3) end it 'is threadsafe' do num_threads = 100 (1..num_threads).to_a.map do Thread.new do Wisper::GlobalListeners.subscribe(Object.new) sleep(rand) # a little chaos end end.each(&:join) expect(Wisper::GlobalListeners.listeners.size).to eq num_threads end end describe '.listeners' do it 'returns collection of global listeners' do Wisper::GlobalListeners.subscribe(global_listener) expect(Wisper::GlobalListeners.listeners).to eq [global_listener] end it 'returns an immutable collection' do expect(Wisper::GlobalListeners.listeners).to be_frozen expect { Wisper::GlobalListeners.listeners << global_listener }.to raise_error(RuntimeError) end end it '.clear clears all global listeners' do Wisper::GlobalListeners.subscribe(global_listener) Wisper::GlobalListeners.clear expect(Wisper::GlobalListeners.listeners).to be_empty end end wisper-2.0.1/lib/0000755000175000017500000000000013620176654013443 5ustar gabstergabsterwisper-2.0.1/lib/wisper/0000755000175000017500000000000013620176654014754 5ustar gabstergabsterwisper-2.0.1/lib/wisper/version.rb0000644000175000017500000000004613620176654016766 0ustar gabstergabstermodule Wisper VERSION = "2.0.1" end wisper-2.0.1/lib/wisper/value_objects/0000755000175000017500000000000013620176654017601 5ustar gabstergabsterwisper-2.0.1/lib/wisper/value_objects/prefix.rb0000644000175000017500000000132113620176654021420 0ustar gabstergabstermodule Wisper module ValueObjects #:nodoc: # Prefix for notifications # # @example # Wisper::ValueObjects::Prefix.new nil # => "" # Wisper::ValueObjects::Prefix.new "when" # => "when_" # Wisper::ValueObjects::Prefix.new true # => "on_" class Prefix < String class << self attr_accessor :default end # @param [true, nil, #to_s] value # # @return [undefined] def initialize(value = nil) super "#{ (value == true) ? default : value }_" replace "" if self == "_" end private def default self.class.default || 'on' end end # class Prefix end # module ValueObjects end # module Wisper wisper-2.0.1/lib/wisper/value_objects/events.rb0000644000175000017500000000303713620176654021435 0ustar gabstergabstermodule Wisper module ValueObjects #:nodoc: # Describes allowed events # # Duck-types the argument to quack like array of strings # when responding to the {#include?} method call. class Events # @!scope class # @!method new(on) # Initializes a 'list' of events # # @param [NilClass, String, Symbol, Array, Regexp] list # # @raise [ArgumentError] # if an argument if of unsupported type # # @return [undefined] def initialize(list) @list = list end # Check if given event is 'included' to the 'list' of events # # @param [#to_s] event # # @return [Boolean] def include?(event) appropriate_method.call(event.to_s) end private def methods { NilClass => ->(_event) { true }, String => ->(event) { list == event }, Symbol => ->(event) { list.to_s == event }, Enumerable => ->(event) { list.map(&:to_s).include? event }, Regexp => ->(event) { list.match(event) || false } } end def list @list end def appropriate_method @appropriate_method ||= methods[recognized_type] end def recognized_type methods.keys.detect(&list.method(:is_a?)) || type_not_recognized end def type_not_recognized fail(ArgumentError, "#{list.class} not supported for `on` argument") end end # class Events end # module ValueObjects end # module Wisper wisper-2.0.1/lib/wisper/temporary_listeners.rb0000644000175000017500000000152213620176654021413 0ustar gabstergabster# Handles temporary global subscriptions # @api private module Wisper class TemporaryListeners def self.subscribe(*listeners, &block) new.subscribe(*listeners, &block) end def self.registrations new.registrations end def subscribe(*listeners, &_block) new_registrations = build_registrations(listeners) begin registrations.merge new_registrations yield ensure registrations.subtract new_registrations end self end def registrations Thread.current[key] ||= Set.new end private def build_registrations(listeners) options = listeners.last.is_a?(Hash) ? listeners.pop : {} listeners.map { |listener| ObjectRegistration.new(listener, options) } end def key '__wisper_temporary_listeners' end end end wisper-2.0.1/lib/wisper/registration/0000755000175000017500000000000013620176654017466 5ustar gabstergabsterwisper-2.0.1/lib/wisper/registration/registration.rb0000644000175000017500000000044713620176654022532 0ustar gabstergabster# @api private module Wisper class Registration attr_reader :on, :listener def initialize(listener, options) @listener = listener @on = ValueObjects::Events.new options[:on] end private def should_broadcast?(event) on.include? event end end end wisper-2.0.1/lib/wisper/registration/object.rb0000644000175000017500000000240513620176654021262 0ustar gabstergabster# @api private module Wisper class ObjectRegistration < Registration attr_reader :with, :prefix, :allowed_classes, :broadcaster def initialize(listener, options) super(listener, options) @with = options[:with] @prefix = ValueObjects::Prefix.new options[:prefix] @allowed_classes = Array(options[:scope]).map(&:to_s).to_set @broadcaster = map_broadcaster(options[:async] || options[:broadcaster]) end def broadcast(event, publisher, *args) method_to_call = map_event_to_method(event) if should_broadcast?(event) && listener.respond_to?(method_to_call) && publisher_in_scope?(publisher) broadcaster.broadcast(listener, publisher, method_to_call, args) end end private def publisher_in_scope?(publisher) allowed_classes.empty? || publisher.class.ancestors.any? { |ancestor| allowed_classes.include?(ancestor.to_s) } end def map_event_to_method(event) prefix + (with || event).to_s end def map_broadcaster(value) return value if value.respond_to?(:broadcast) value = :async if value == true value = :default if value == nil configuration.broadcasters.fetch(value) end def configuration Wisper.configuration end end end wisper-2.0.1/lib/wisper/registration/block.rb0000644000175000017500000000031513620176654021104 0ustar gabstergabster# @api private module Wisper class BlockRegistration < Registration def broadcast(event, publisher, *args) if should_broadcast?(event) listener.call(*args) end end end end wisper-2.0.1/lib/wisper/publisher.rb0000644000175000017500000000373213620176654017303 0ustar gabstergabstermodule Wisper module Publisher def listeners registrations.map(&:listener).freeze end # subscribe a listener # # @example # my_publisher.subscribe(MyListener.new) # # @return [self] def subscribe(listener, options = {}) raise ArgumentError, "#{__method__} does not take a block, did you mean to call #on instead?" if block_given? local_registrations << ObjectRegistration.new(listener, options) self end # subscribe a block # # @example # my_publisher.on(:order_created) { |args| ... } # # @return [self] def on(*events, &block) raise ArgumentError, 'must give at least one event' if events.empty? raise ArgumentError, 'must pass a block' if !block local_registrations << BlockRegistration.new(block, on: events) self end # broadcasts an event # # @example # def call # # ... # broadcast(:finished, self) # end # # @return [self] def broadcast(event, *args) registrations.each do | registration | registration.broadcast(clean_event(event), self, *args) end self end alias :publish :broadcast private :broadcast, :publish module ClassMethods # subscribe a listener # # @example # MyPublisher.subscribe(MyListener.new) # def subscribe(listener, options = {}) GlobalListeners.subscribe(listener, options.merge(:scope => self)) end end private def local_registrations @local_registrations ||= Set.new end def global_registrations GlobalListeners.registrations end def temporary_registrations TemporaryListeners.registrations end def registrations local_registrations + global_registrations + temporary_registrations end def clean_event(event) event.to_s.gsub('-', '_') end def self.included(base) base.extend(ClassMethods) end end end wisper-2.0.1/lib/wisper/global_listeners.rb0000644000175000017500000000243313620176654020633 0ustar gabstergabster# Handles global subscriptions # @api private require 'singleton' module Wisper class GlobalListeners include Singleton def initialize @registrations = Set.new @mutex = Mutex.new end def subscribe(*listeners) options = listeners.last.is_a?(Hash) ? listeners.pop : {} with_mutex do listeners.each do |listener| @registrations << ObjectRegistration.new(listener, options) end end self end def unsubscribe(*listeners) with_mutex do @registrations.delete_if do |registration| listeners.include?(registration.listener) end end self end def registrations with_mutex { @registrations } end def listeners registrations.map(&:listener).freeze end def clear with_mutex { @registrations.clear } end def self.subscribe(*listeners) instance.subscribe(*listeners) end def self.unsubscribe(*listeners) instance.unsubscribe(*listeners) end def self.registrations instance.registrations end def self.listeners instance.listeners end def self.clear instance.clear end private def with_mutex @mutex.synchronize { yield } end end end wisper-2.0.1/lib/wisper/configuration.rb0000644000175000017500000000164013620176654020151 0ustar gabstergabsterrequire 'forwardable' module Wisper class Configuration attr_reader :broadcasters def initialize @broadcasters = Broadcasters.new end # registers a broadcaster, referenced by key # # @param key [String, #to_s] an arbitrary key # @param broadcaster [#broadcast] a broadcaster def broadcaster(key, broadcaster) @broadcasters[key] = broadcaster self end # sets the default value for prefixes # # @param [#to_s] value # # @return [String] def default_prefix=(value) ValueObjects::Prefix.default = value end class Broadcasters extend Forwardable def_delegators :@data, :[], :[]=, :empty?, :include?, :clear, :keys, :to_h def initialize @data = {} end def fetch(key) raise KeyError, "broadcaster not found for #{key}" unless include?(key) @data[key] end end end end wisper-2.0.1/lib/wisper/broadcasters/0000755000175000017500000000000013620176654017430 5ustar gabstergabsterwisper-2.0.1/lib/wisper/broadcasters/send_broadcaster.rb0000644000175000017500000000027313620176654023261 0ustar gabstergabstermodule Wisper module Broadcasters class SendBroadcaster def broadcast(listener, publisher, event, args) listener.public_send(event, *args) end end end end wisper-2.0.1/lib/wisper/broadcasters/logger_broadcaster.rb0000644000175000017500000000221713620176654023607 0ustar gabstergabster# Provides a way of wrapping another broadcaster with logging module Wisper module Broadcasters class LoggerBroadcaster def initialize(logger, broadcaster) @logger = logger @broadcaster = broadcaster end def broadcast(listener, publisher, event, args) @logger.info("[WISPER] #{name(publisher)} published #{event} to #{name(listener)} with #{args_info(args)}") @broadcaster.broadcast(listener, publisher, event, args) end private def name(object) id_method = %w(id uuid key object_id).find do |method_name| object.respond_to?(method_name) && object.method(method_name).arity <= 0 end id = object.send(id_method) class_name = object.class == Class ? object.name : object.class.name "#{class_name}##{id}" end def args_info(args) return 'no arguments' if args.empty? args.map do |arg| arg_string = name(arg) arg_string += ": #{arg.inspect}" if [Numeric, Array, Hash, String].any? {|klass| arg.is_a?(klass) } arg_string end.join(', ') end end end end wisper-2.0.1/lib/wisper.rb0000644000175000017500000000264413620176654015307 0ustar gabstergabsterrequire 'set' require 'wisper/version' require 'wisper/configuration' require 'wisper/publisher' require 'wisper/value_objects/prefix' require 'wisper/value_objects/events' require 'wisper/registration/registration' require 'wisper/registration/object' require 'wisper/registration/block' require 'wisper/global_listeners' require 'wisper/temporary_listeners' require 'wisper/broadcasters/send_broadcaster' require 'wisper/broadcasters/logger_broadcaster' module Wisper # Examples: # # Wisper.subscribe(AuditRecorder.new) # # Wisper.subscribe(AuditRecorder.new, StatsRecorder.new) # # Wisper.subscribe(AuditRecorder.new, on: 'order_created') # # Wisper.subscribe(AuditRecorder.new, scope: 'MyPublisher') # # Wisper.subscribe(AuditRecorder.new, StatsRecorder.new) do # # .. # end # def self.subscribe(*args, &block) if block_given? TemporaryListeners.subscribe(*args, &block) else GlobalListeners.subscribe(*args) end end def self.unsubscribe(*listeners) GlobalListeners.unsubscribe(*listeners) end def self.publisher Publisher end def self.clear GlobalListeners.clear end def self.configure yield(configuration) end def self.configuration @configuration ||= Configuration.new end def self.setup configure do |config| config.broadcaster(:default, Broadcasters::SendBroadcaster.new) end end end Wisper.setup wisper-2.0.1/gem-public_cert.pem0000644000175000017500000000224413620176654016443 0ustar gabstergabster-----BEGIN CERTIFICATE----- MIIDQDCCAiigAwIBAgIBATANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQDDBprcmlz LmxlZWNoL0RDPWdtYWlsL0RDPWNvbTAeFw0xOTEwMTYxNDEzNDVaFw0yMDEwMTUx NDEzNDVaMCUxIzAhBgNVBAMMGmtyaXMubGVlY2gvREM9Z21haWwvREM9Y29tMIIB IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8gg/iZE6RFkl6GjisJJf9wQl 5Tog+mrGqtylNpe9QKeyQS1fKLNR3Cqmv6sT3f3GlU8hhG+802MXmlJo7VyENs+s HdMy85fBnYwhS/szhTzBqduXw43RAAGqB0VaHAoHdufTZBkmFSXET5c0/jUqQrPL Zxsm2i0NV8cVpvrZlQcsW4Kf97DG1ZFNncS0IfCDqnluh21qMSgVAuiKHGoVKCYG Igey486VuuJ8j6axwW3ii8HoBOxjF8Ka/rJ/t4sB9Ql/p6RHR4RhxP5xIS9geCpI dsPGnCSleTW1EnXGHLdOdJpVmJXueYJEQJ1Rh/rR+9PeqAT3RA0D/dxSGIVtrQID AQABo3sweTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUyupX2gb3 +isBRwf4/fhgLnh02sYwHwYDVR0RBBgwFoEUa3Jpcy5sZWVjaEBnbWFpbC5jb20w HwYDVR0SBBgwFoEUa3Jpcy5sZWVjaEBnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD ggEBAExirH+Py0Wm8+ZzNnx/oyFXSF7V9FClYhPUXwQ5FmvQG+gXRlonO9IR/HL+ wGJNFh4B19fpughgQeJKjKJsG2zk2XnSK2yQyyNGdr/9dHXp/TS5sGvX9jFYVeAr 0XCWXRBK+IfEqNnbFvPkwZEK62WLnrL50gMs3u2ILmLP5gkO3EoLyezJvIj33CLO HX0Plp0VwiSp8+gEELX5wtdrnJ/UjmrY1lg7szukE+jgOJ++1e7Rzr+woBewIWvF xn3z1ObkNQCnLZMRmzEcSa40T10/AuuuEv32emAgb50u9vz+s4y2lnE9i4mWW7+X 5vdRgX7VnzYPA+Lc39kD+AcGIrQ= -----END CERTIFICATE----- wisper-2.0.1/bin/0000755000175000017500000000000013620176654013445 5ustar gabstergabsterwisper-2.0.1/bin/setup0000755000175000017500000000011213620176654014525 0ustar gabstergabster#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install wisper-2.0.1/bin/console0000755000175000017500000000013013620176654015027 0ustar gabstergabster#!/usr/bin/env ruby require "bundler/setup" require "wisper" require "pry" Pry.start wisper-2.0.1/Rakefile0000644000175000017500000000033413620176654014342 0ustar gabstergabsterrequire "bundler/gem_tasks" require 'rspec/core/rake_task' require 'coveralls/rake/task' RSpec::Core::RakeTask.new Coveralls::RakeTask.new task :test_with_coveralls => [:spec, 'coveralls:push'] task :default => :spec wisper-2.0.1/README.md0000644000175000017500000002561513620176654014165 0ustar gabstergabster# Wisper *A micro library providing Ruby objects with Publish-Subscribe capabilities* [![Gem Version](https://badge.fury.io/rb/wisper.svg)](http://badge.fury.io/rb/wisper) [![Code Climate](https://codeclimate.com/github/krisleech/wisper.svg)](https://codeclimate.com/github/krisleech/wisper) [![Build Status](https://travis-ci.org/krisleech/wisper.svg?branch=master)](https://travis-ci.org/krisleech/wisper) [![Coverage Status](https://coveralls.io/repos/krisleech/wisper/badge.svg?branch=master)](https://coveralls.io/r/krisleech/wisper?branch=master) * Decouple core business logic from external concerns in Hexagonal style architectures * Use as an alternative to ActiveRecord callbacks and Observers in Rails apps * Connect objects based on context without permanence * Publish events synchronously or asynchronously Note: Wisper was originally extracted from a Rails codebase but is not dependant on Rails. Please also see the [Wiki](https://github.com/krisleech/wisper/wiki) for more additional information and articles. ## Installation Add this line to your application's Gemfile: ```ruby gem 'wisper', '2.0.0' ``` ## Usage Any class with the `Wisper::Publisher` module included can broadcast events to subscribed listeners. Listeners subscribe, at runtime, to the publisher. ### Publishing ```ruby class CancelOrder include Wisper::Publisher def call(order_id) order = Order.find_by_id(order_id) # business logic... if order.cancelled? broadcast(:cancel_order_successful, order.id) else broadcast(:cancel_order_failed, order.id) end end end ``` When a publisher broadcasts an event it can include any number of arguments. The `broadcast` method is also aliased as `publish`. You can also include `Wisper.publisher` instead of `Wisper::Publisher`. ### Subscribing #### Objects Any object can be subscribed as a listener. ```ruby cancel_order = CancelOrder.new cancel_order.subscribe(OrderNotifier.new) cancel_order.call(order_id) ``` The listener would need to implement a method for every event it wishes to receive. ```ruby class OrderNotifier def cancel_order_successful(order_id) order = Order.find_by_id(order_id) # notify someone ... end end ``` #### Blocks Blocks can be subscribed to single events and can be chained. ```ruby cancel_order = CancelOrder.new cancel_order.on(:cancel_order_successful) { |order_id| ... } .on(:cancel_order_failed) { |order_id| ... } cancel_order.call(order_id) ``` You can also subscribe to multiple events using `on` by passing additional events as arguments. ```ruby cancel_order = CancelOrder.new cancel_order.on(:cancel_order_successful) { |order_id| ... } .on(:cancel_order_failed, :cancel_order_invalid) { |order_id| ... } cancel_order.call(order_id) ``` Do not `return` from inside a subscribed block, due to the way [Ruby treats blocks](http://product.reverb.com/2015/02/28/the-strange-case-of-wisper-and-ruby-blocks-behaving-like-procs/) this will prevent any subsequent listeners having their events delivered. ### Handling Events Asynchronously ```ruby cancel_order.subscribe(OrderNotifier.new, async: true) ``` Wisper has various adapters for asynchronous event handling, please refer to [wisper-celluloid](https://github.com/krisleech/wisper-celluloid), [wisper-sidekiq](https://github.com/krisleech/wisper-sidekiq), [wisper-activejob](https://github.com/krisleech/wisper-activejob), [wisper-que](https://github.com/joevandyk/wisper-que) or [wisper-resque](https://github.com/bzurkowski/wisper-resque). Depending on the adapter used the listener may need to be a class instead of an object. In this situation, every method corresponding to events should be declared as a class method, too. For example: ```ruby class OrderNotifier # declare a class method if you are subscribing the listener class instead of its instance like: # cancel_order.subscribe(OrderNotifier) # def self.cancel_order_successful(order_id) order = Order.find_by_id(order_id) # notify someone ... end end ``` ### ActionController ```ruby class CancelOrderController < ApplicationController def create cancel_order = CancelOrder.new cancel_order.subscribe(OrderMailer, async: true) cancel_order.subscribe(ActivityRecorder, async: true) cancel_order.subscribe(StatisticsRecorder, async: true) cancel_order.on(:cancel_order_successful) { |order_id| redirect_to order_path(order_id) } cancel_order.on(:cancel_order_failed) { |order_id| render action: :new } cancel_order.call(order_id) end end ``` ### ActiveRecord If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks: ```ruby class Order < ActiveRecord::Base include Wisper::Publisher after_commit :publish_creation_successful, on: :create after_validation :publish_creation_failed, on: :create private def publish_creation_successful broadcast(:order_creation_successful, self) end def publish_creation_failed broadcast(:order_creation_failed, self) if errors.any? end end ``` There are more examples in the [Wiki](https://github.com/krisleech/wisper/wiki). ## Global Listeners Global listeners receive all broadcast events which they can respond to. This is useful for cross cutting concerns such as recording statistics, indexing, caching and logging. ```ruby Wisper.subscribe(MyListener.new) ``` In a Rails app you might want to add your global listeners in an initializer. Global listeners are threadsafe. Subscribers will receive events published on all threads. ### Scoping by publisher class You might want to globally subscribe a listener to publishers with a certain class. ```ruby Wisper.subscribe(MyListener.new, scope: :MyPublisher) Wisper.subscribe(MyListener.new, scope: MyPublisher) Wisper.subscribe(MyListener.new, scope: "MyPublisher") Wisper.subscribe(MyListener.new, scope: [:MyPublisher, :MyOtherPublisher]) ``` This will subscribe the listener to all instances of the specified class(es) and their subclasses. Alternatively you can also do exactly the same with a publisher class itself: ```ruby MyPublisher.subscribe(MyListener.new) ``` ## Temporary Global Listeners You can also globally subscribe listeners for the duration of a block. ```ruby Wisper.subscribe(MyListener.new, OtherListener.new) do # do stuff end ``` Any events broadcast within the block by any publisher will be sent to the listeners. This is useful for capturing events published by objects to which you do not have access in a given context. Temporary Global Listeners are threadsafe. Subscribers will receive events published on the same thread. ## Subscribing to selected events 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 a string, symbol, array or regular expression to `on`: ```ruby post_creator.subscribe(PusherListener.new, on: :create_post_successful) ``` ## Prefixing broadcast events If you would prefer listeners to receive events with a prefix, for example `on`, you can do so by passing a string or symbol to `prefix:`. ```ruby post_creator.subscribe(PusherListener.new, prefix: :on) ``` If `post_creator` were to broadcast the event `post_created` the subscribed listeners would receive `on_post_created`. You can also pass `true` which will use the default prefix, "on". ## Mapping an event to a different method By default the method called on the listener is the same as the event broadcast. However it can be mapped to a different method using `with:`. ```ruby report_creator.subscribe(MailResponder.new, with: :successful) ``` This is pretty useless unless used in conjunction with `on:`, since all events will get mapped to `:successful`. Instead you might do something like this: ```ruby report_creator.subscribe(MailResponder.new, on: :create_report_successful, with: :successful) ``` If you pass an array of events to `on:` each event will be mapped to the same method when `with:` is specified. If you need to listen for select events _and_ map each one to a different method subscribe the listener once for each mapping: ```ruby report_creator.subscribe(MailResponder.new, on: :create_report_successful, with: :successful) report_creator.subscribe(MailResponder.new, on: :create_report_failed, with: :failed) ``` You could also alias the method within your listener, as such `alias successful create_report_successful`. ## Testing Testing matchers and stubs are in separate gems. * [wisper-rspec](https://github.com/krisleech/wisper-rspec) * [wisper-minitest](https://github.com/digitalcuisine/wisper-minitest) ### Clearing Global Listeners If you use global listeners in non-feature tests you _might_ want to clear them in a hook to prevent global subscriptions persisting between tests. ```ruby after { Wisper.clear } ``` ## Need help? The [Wiki](https://github.com/krisleech/wisper/wiki) has more examples, articles and talks. Got a specific question, try the [Wisper tag on StackOverflow](http://stackoverflow.com/questions/tagged/wisper). ## Compatibility Tested with MRI 2.x, JRuby and Rubinius. See the [build status](https://travis-ci.org/krisleech/wisper) for details. ## Running Specs ``` bundle exec rspec ``` To run the specs on code changes try [entr](http://entrproject.org/): ``` ls **/*.rb | entr bundle exec rspec ``` ## Contributing Please read the [Contributing Guidelines](https://github.com/krisleech/wisper/blob/master/CONTRIBUTING.md). ## Security * gem releases are [signed](http://guides.rubygems.org/security/) ([public key](https://github.com/krisleech/wisper/blob/master/gem-public_cert.pem)) * commits are GPG signed ([public key](https://pgp.mit.edu/pks/lookup?op=get&search=0x3ABC74851F7CCC88)) * My [Keybase.io profile](https://keybase.io/krisleech) ## License (The MIT License) Copyright (c) 2013 Kris Leech Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wisper-2.0.1/Gemfile0000644000175000017500000000023313620176654014166 0ustar gabstergabstersource 'https://rubygems.org' gemspec gem 'rake' gem 'rspec' gem 'coveralls', require: false group :extras do gem 'flay' gem 'pry' gem 'yard' end wisper-2.0.1/CONTRIBUTING.md0000644000175000017500000000406013620176654015126 0ustar gabstergabster# How to Contribute We very much welcome contributions to Wisper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## Getting started Please first check the existing [Issues](https://github.com/krisleech/wisper/issues) and [Pull Requests](https://github.com/krisleech/wisper/pulls) to ensure your issue has not already been discused. ## Bugs Please submit a bug report to the issue tracker, with the version of Wisper and Ruby you are using and a small code sample (or better yet a failing test). ## Features Please open an issue with your proposed feature. We can discuss the feature and if it is acceptable we can also discuss implimentation details. You will in most cases have to submit a PR which adds the feature. Wisper is a micro library and will remain lean. Some features would be most appropriate as an extension to Wisper. We also have a [Gitter channel](https://gitter.im/krisleech/wisper) if you wish to discuss your ideas. ## Questions Try the [Wiki](https://github.com/krisleech/wisper/wiki) first, the examples and how to sections have lots of information. Please ask questions on StackOverflow, [tagged wisper](https://stackoverflow.com/questions/tagged/wisper). Feel free to ping me the URL on [Twitter](https://twitter.com/krisleech). ## Pull requests * Fork the project, create a new branch `master`. * Squash commits which are related. * Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) * Documentation only changes should have `[skip ci]` in the commit message * Follow existing code style in terms of syntax, indentation etc. * Add an entry to the CHANGELOG * Do not bump the VERSION, but do indicate in the CHANGELOG if the change is not backwards compatible. * Issue a Pull Request ## Versions The `v1` branch is a long lived branch and you should branch from this if you wish to fix an issue in version `~> 1.0`. Otherwise branch from `master` branch. wisper-2.0.1/CHANGELOG.md0000644000175000017500000001034613620176654014512 0ustar gabstergabster## HEAD (unreleased) ## 2.0.1 (29th Aug 2019) Authors: David Wilkie, hosseintoussi, Maxim Polunin, Tristan Harmer, Bartosz Żurkowski, Kris Leech * fix: support nested temporary global listeners * docs: add threadsafe notes to README for global and temporary listeners * docs: fix spelling mistakes in README * fix: safely get signing key in gemspec when HOME is not set * adds: add console to aid experiments and REPL driven development * adds: Latest Ruby to CI ## 2.0.0 (7th Mar 2017) Authors: Sergey Mostovoy, Andrew Kozin, Kyle Tolle, Martin, Rob Miller, Mike Dalto, orthographic-pedant, Drew Ulmer, Mikey Hogarth, Attila Domokos, Josh Miltz, Pascal Betz, Vasily Kolesnikov, Julien Letessier, Kris Leech * Fix: logger raises exception if hash is passed as an argument to a listener #133, #136 * Fix: deprecation warnings #120 * Doc improvements: #106, #111, #116, #122, #128, #130, #147, #149, #150, #151 * Adds: Allow configuration of default prefix when using `prefix: true`. #105 * Adds: Allow unsubscribing of global listeners #118 * Adds: Helpful error message when passing a block to `#subscribe` of `#on` #125 * Adds: raise an error message when `#on` is not passed a block #146 * Adds: Support for JRuby 9.x #148 * Adds: Support for MRI 2.4.0 #155 * Refactor specs #126, #131 ## 2.0.0.rc1 (17 Dec 2014) Authors: Kris Leech * remove: deprecated methods * remove: rspec matcher and stubbing (moved to [wisper-rspec](https://github.com/krisleech/wisper-rspec)) * feature: add regexp support to `on` argument * remove: announce alias for broadcasting * docs: add Code of Conduct * drop support for Ruby 1.9 ## 1.6.0 (25 Oct 2014) Authors: Kris Leech * deprecate: add_listener, add_block_listener and respond_to * internal: make method naming more consistent ## 1.5.0 (6th Oct 2014) Authors: Kris Leech * feature: allow events to be published asynchronously * feature: broadcasting of events is plugable and configurable * feature: broadcasters can be aliased via a symbol * feature: logging broadcaster ## 1.4.0 (8th Sept 2014) Authors: Kris Leech, Marc Ignacio, Ahmed Abdel Razzak, kmehkeri, Jake Hoffner * feature: matcher for rspec 3 * fix: temporary global listeners are cleared if an exception is raised * refactor: update all specs to rspec 3 expect syntax * docs: update README to rspec 3 expect syntax * feature: combine global and temporary listener methods as `Wisper.subscribe` * deprecate: `Wisper.add_listener` and `Wisper.with_listeners,` use `Wisper.subscribe` instead ## 1.3.0 (18th Jan 2014) Authors: Kris Leech, Yan Pritzker, Charlie Tran * feature: global subscriptions can be scoped to a class (and sub-classes) * upgrade: use rspec 3 * feature: allow prefixing of events with 'on' * feature: Allow stubbed publisher method to accept arbitrary args ## 1.2.1 (7th Oct 2013) Authors: Kris Leech, Tomasz Szymczyszyn, Alex Heeton * feature: global subscriptions can be passed options * docs: improve README examples * docs: add license to gemspec ## 1.2.0 (21st July 2013) Authors: Kris Leech, Darren Coxall * feature: support for multiple events at once * fix: clear global listeners after each spec ## 1.1.0 (7th June 2013) Authors: Kris Leech, chatgris * feature: add temporary global listeners * docs: improve ActiveRecord example * refactor: improve specs * upgrade: add Ruby 2.0 support * fix: make listener collection immutable * remove: async publishing and Celluloid dependency * fix: Make global listeners getter and setter threadsafe [9] ## 1.0.1 (2nd May 2013) Authors: Kris Leech, Yan Pritzker * feature: add async publishing using Celluloid * docs: improve README examples * feature: `stub_wisper_publisher` rspec helper * feature: global listeners * refactor: improve specs ## 1.0.0 (7th April 2013) Authors: Kris Leech * refactor: specs * refactor: registrations * feature: Add `with` argument to `subscribe` * docs: improve README examples * feature: Allow subscriptions to be chainable * feature: Add `on` syntax for block subscription * remove: Remove support for Ruby 1.8.7 * docs: Add badges to README ## 0.0.2 (30th March 2013) Authors: Kris Leech * remove: ActiveSupport dependency * docs: fix syntax highlighting in README ## 0.0.1 (30th March 2013) Authors: Kris Leech * docs: add README * feature: registration of objects and blocks wisper-2.0.1/.travis.yml0000644000175000017500000000062113620176654015005 0ustar gabstergabsterlanguage: ruby before_install: - gem update --system - gem update bundler bundler_args: --without=extras script: rspec spec sudo: false rvm: - 2.6.3 - 2.5.5 - 2.4.6 - jruby-9.2 # - rbx-2 ### ALLOWED FAILURES ### # see how compatible we are with dev versions, but do not fail the build - ruby-head - jruby-head matrix: allow_failures: - rvm: ruby-head - rvm: jruby-head wisper-2.0.1/.rspec0000644000175000017500000000007313620176654014012 0ustar gabstergabster--color --format progress --require spec_helper --warnings wisper-2.0.1/.gitignore0000644000175000017500000000026413620176654014667 0ustar gabstergabster*.gem *.rbc .ruby-version .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp tags vendor wisper-2.0.1/checksums.yaml.gz.sig0000444000175000017500000000040013620176654016736 0ustar gabstergabster ͜.4O !A#ǂA³ bZ%/H"c)? %tS\xߐɃqBLc7e)!C!i<84qW y\$okZsce 6IjSS!/r|_}KJxgEd sʘEnkvioRܯye:,^3nDk&% raX`J(“]Awisper-2.0.1/data.tar.gz.sig0000444000175000017500000000040013620176654015506 0ustar gabstergabstervtrƷ?_x!B]yMn6BA@ﰜ?~Q:ҧKT-B;B |mJk\2j%- ,7fލ#]s uc^w?ƀਤ^͊H1 _ܯo2B)(&}oGbȓ+^<=_XjXT:9M!ZUp !tdX&"?VXг3Ywisper-2.0.1/metadata.gz.sig0000444000175000017500000000040013620176654015570 0ustar gabstergabster0 !6+!3 UyZ\"=* ƒG]d+Ag,bF`)Pl27C]B q3{]\y͑7>K[w[%R͒ÉW}CaYuR 'jr9LtVC 1!b0̵9#j.y^z*DbnYj_vk;k` Zjm ]QV7wU"vըQ|eڝ݆?