siftery-wisper 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +22 -0
  5. data/CHANGELOG.md +129 -0
  6. data/CONTRIBUTING.md +61 -0
  7. data/Gemfile +13 -0
  8. data/README.md +373 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +8 -0
  11. data/bin/setup +6 -0
  12. data/gem-public_cert.pem +21 -0
  13. data/lib/wisper.rb +65 -0
  14. data/lib/wisper/broadcasters/logger_broadcaster.rb +37 -0
  15. data/lib/wisper/broadcasters/send_broadcaster.rb +9 -0
  16. data/lib/wisper/configuration.rb +44 -0
  17. data/lib/wisper/global_listeners.rb +74 -0
  18. data/lib/wisper/publisher.rb +89 -0
  19. data/lib/wisper/registration/block.rb +11 -0
  20. data/lib/wisper/registration/object.rb +77 -0
  21. data/lib/wisper/registration/registration.rb +19 -0
  22. data/lib/wisper/temporary_listeners.rb +41 -0
  23. data/lib/wisper/value_objects/events.rb +61 -0
  24. data/lib/wisper/value_objects/prefix.rb +29 -0
  25. data/lib/wisper/version.rb +3 -0
  26. data/siftery-wisper.gemspec +31 -0
  27. data/spec/lib/global_listeners_spec.rb +82 -0
  28. data/spec/lib/integration_spec.rb +56 -0
  29. data/spec/lib/simple_example_spec.rb +21 -0
  30. data/spec/lib/temporary_global_listeners_spec.rb +67 -0
  31. data/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb +93 -0
  32. data/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb +28 -0
  33. data/spec/lib/wisper/configuration/broadcasters_spec.rb +11 -0
  34. data/spec/lib/wisper/configuration_spec.rb +36 -0
  35. data/spec/lib/wisper/publisher_spec.rb +331 -0
  36. data/spec/lib/wisper/registrations/object_spec.rb +14 -0
  37. data/spec/lib/wisper/value_objects/events_spec.rb +107 -0
  38. data/spec/lib/wisper/value_objects/prefix_spec.rb +46 -0
  39. data/spec/lib/wisper_spec.rb +99 -0
  40. data/spec/spec_helper.rb +24 -0
  41. metadata +101 -0
@@ -0,0 +1,19 @@
1
+ # @api private
2
+
3
+ module Wisper
4
+ class Registration
5
+ attr_reader :on, :listener, :options
6
+
7
+ def initialize(listener, options)
8
+ @listener = listener
9
+ @options = options
10
+ @on = ValueObjects::Events.new options[:on]
11
+ end
12
+
13
+ private
14
+
15
+ def should_broadcast?(event)
16
+ on.include? event
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # Handles temporary global subscriptions
2
+
3
+ # @api private
4
+
5
+ module Wisper
6
+ class TemporaryListeners
7
+
8
+ def self.subscribe(*listeners, &block)
9
+ new.subscribe(*listeners, &block)
10
+ end
11
+
12
+ def self.registrations
13
+ new.registrations
14
+ end
15
+
16
+ def subscribe(*listeners, &block)
17
+ options = listeners.last.is_a?(Hash) ? listeners.pop : {}
18
+ begin
19
+ listeners.each { |listener| registrations << ObjectRegistration.new(listener, options) }
20
+ yield
21
+ ensure
22
+ clear
23
+ end
24
+ self
25
+ end
26
+
27
+ def registrations
28
+ Thread.current[key] ||= Set.new
29
+ end
30
+
31
+ private
32
+
33
+ def clear
34
+ registrations.clear
35
+ end
36
+
37
+ def key
38
+ '__wisper_temporary_listeners'
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,61 @@
1
+ module Wisper
2
+ module ValueObjects #:nodoc:
3
+ # Describes allowed events
4
+ #
5
+ # Duck-types the argument to quack like array of strings
6
+ # when responding to the {#include?} method call.
7
+ class Events
8
+
9
+ # @!scope class
10
+ # @!method new(on)
11
+ # Initializes a 'list' of events
12
+ #
13
+ # @param [NilClass, String, Symbol, Array, Regexp] list
14
+ #
15
+ # @raise [ArgumentError]
16
+ # if an argument if of unsupported type
17
+ #
18
+ # @return [undefined]
19
+ def initialize(list)
20
+ @list = list
21
+ end
22
+
23
+ # Check if given event is 'included' to the 'list' of events
24
+ #
25
+ # @param [#to_s] event
26
+ #
27
+ # @return [Boolean]
28
+ def include?(event)
29
+ appropriate_method.call(event.to_s)
30
+ end
31
+
32
+ private
33
+
34
+ def methods
35
+ {
36
+ NilClass => ->(_event) { true },
37
+ String => ->(event) { list == event },
38
+ Symbol => ->(event) { list.to_s == event },
39
+ Enumerable => ->(event) { list.map(&:to_s).include? event },
40
+ Regexp => ->(event) { list.match(event) || false }
41
+ }
42
+ end
43
+
44
+ def list
45
+ @list
46
+ end
47
+
48
+ def appropriate_method
49
+ @appropriate_method ||= methods[recognized_type]
50
+ end
51
+
52
+ def recognized_type
53
+ methods.keys.detect(&list.method(:is_a?)) || type_not_recognized
54
+ end
55
+
56
+ def type_not_recognized
57
+ fail(ArgumentError, "#{list.class} not supported for `on` argument")
58
+ end
59
+ end # class Events
60
+ end # module ValueObjects
61
+ end # module Wisper
@@ -0,0 +1,29 @@
1
+ module Wisper
2
+ module ValueObjects #:nodoc:
3
+ # Prefix for notifications
4
+ #
5
+ # @example
6
+ # Wisper::ValueObjects::Prefix.new nil # => ""
7
+ # Wisper::ValueObjects::Prefix.new "when" # => "when_"
8
+ # Wisper::ValueObjects::Prefix.new true # => "on_"
9
+ class Prefix < String
10
+ class << self
11
+ attr_accessor :default
12
+ end
13
+
14
+ # @param [true, nil, #to_s] value
15
+ #
16
+ # @return [undefined]
17
+ def initialize(value = nil)
18
+ super "#{ (value == true) ? default : value }_"
19
+ replace "" if self == "_"
20
+ end
21
+
22
+ private
23
+
24
+ def default
25
+ self.class.default || 'on'
26
+ end
27
+ end # class Prefix
28
+ end # module ValueObjects
29
+ end # module Wisper
@@ -0,0 +1,3 @@
1
+ module Wisper
2
+ VERSION = "2.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'wisper/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "siftery-wisper"
8
+ gem.version = Wisper::VERSION
9
+ gem.authors = ["Shubham Kumar"]
10
+ gem.email = ["shubh2336@gmail.com"]
11
+ gem.description = <<-DESC
12
+ A micro library providing objects with Publish-Subscribe capabilities.
13
+ Both synchronous (in-process) and asynchronous (out-of-process) subscriptions are supported.
14
+ Check out the Wiki for articles, guides and examples: https://github.com/shubh2336/wisper/wiki
15
+ DESC
16
+ gem.summary = "A micro library providing objects with Publish-Subscribe capabilities"
17
+ gem.homepage = "https://github.com/shubh2336/wisper"
18
+ gem.license = "MIT"
19
+
20
+ signing_key = File.expand_path(ENV['HOME'].to_s + '/.ssh/gem-private_key.pem')
21
+
22
+ if File.exist?(signing_key)
23
+ gem.signing_key = signing_key
24
+ gem.cert_chain = ['gem-public_cert.pem']
25
+ end
26
+
27
+ gem.files = `git ls-files`.split($/)
28
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
29
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
30
+ gem.require_paths = ["lib"]
31
+ end
@@ -0,0 +1,82 @@
1
+ describe Wisper::GlobalListeners do
2
+ let(:global_listener) { double('listener') }
3
+ let(:local_listener) { double('listener') }
4
+ let(:publisher) { publisher_class.new }
5
+
6
+ describe '.subscribe' do
7
+ it 'adds given listener to every publisher' do
8
+ Wisper::GlobalListeners.subscribe(global_listener)
9
+ expect(global_listener).to receive(:it_happened)
10
+ publisher.send(:broadcast, :it_happened)
11
+ end
12
+
13
+ it 'works with options' do
14
+ Wisper::GlobalListeners.subscribe(global_listener, :on => :it_happened,
15
+ :with => :woot)
16
+ expect(global_listener).to receive(:woot).once
17
+ expect(global_listener).not_to receive(:it_happened_again)
18
+ publisher.send(:broadcast, :it_happened)
19
+ publisher.send(:broadcast, :it_happened_again)
20
+ end
21
+
22
+ it 'works along side local listeners' do
23
+ # global listener
24
+ Wisper::GlobalListeners.subscribe(global_listener)
25
+
26
+ # local listener
27
+ publisher.subscribe(local_listener)
28
+
29
+ expect(global_listener).to receive(:it_happened)
30
+ expect(local_listener).to receive(:it_happened)
31
+
32
+ publisher.send(:broadcast, :it_happened)
33
+ end
34
+
35
+ it 'can be scoped to classes' do
36
+ publisher_1 = publisher_class.new
37
+ publisher_2 = publisher_class.new
38
+ publisher_3 = publisher_class.new
39
+
40
+ Wisper::GlobalListeners.subscribe(global_listener, :scope => [publisher_1.class,
41
+ publisher_2.class])
42
+
43
+ expect(global_listener).to receive(:it_happened_1).once
44
+ expect(global_listener).to receive(:it_happened_2).once
45
+ expect(global_listener).not_to receive(:it_happened_3)
46
+
47
+ publisher_1.send(:broadcast, :it_happened_1)
48
+ publisher_2.send(:broadcast, :it_happened_2)
49
+ publisher_3.send(:broadcast, :it_happened_3)
50
+ end
51
+
52
+ it 'is threadsafe' do
53
+ num_threads = 100
54
+ (1..num_threads).to_a.map do
55
+ Thread.new do
56
+ Wisper::GlobalListeners.subscribe(Object.new)
57
+ sleep(rand) # a little chaos
58
+ end
59
+ end.each(&:join)
60
+
61
+ expect(Wisper::GlobalListeners.listeners.size).to eq num_threads
62
+ end
63
+ end
64
+
65
+ describe '.listeners' do
66
+ it 'returns collection of global listeners' do
67
+ Wisper::GlobalListeners.subscribe(global_listener)
68
+ expect(Wisper::GlobalListeners.listeners).to eq [global_listener]
69
+ end
70
+
71
+ it 'returns an immutable collection' do
72
+ expect(Wisper::GlobalListeners.listeners).to be_frozen
73
+ expect { Wisper::GlobalListeners.listeners << global_listener }.to raise_error(RuntimeError)
74
+ end
75
+ end
76
+
77
+ it '.clear clears all global listeners' do
78
+ Wisper::GlobalListeners.subscribe(global_listener)
79
+ Wisper::GlobalListeners.clear
80
+ expect(Wisper::GlobalListeners.listeners).to be_empty
81
+ end
82
+ end
@@ -0,0 +1,56 @@
1
+ # Example
2
+ class MyCommand
3
+ include Wisper::Publisher
4
+
5
+ def execute(be_successful)
6
+ if be_successful
7
+ broadcast('success', 'hello')
8
+ else
9
+ broadcast('failure', 'world')
10
+ end
11
+ end
12
+ end
13
+
14
+ describe Wisper do
15
+
16
+ it 'subscribes object to all published events' do
17
+ listener = double('listener')
18
+ expect(listener).to receive(:success).with('hello')
19
+
20
+ command = MyCommand.new
21
+
22
+ command.subscribe(listener)
23
+
24
+ command.execute(true)
25
+ end
26
+
27
+ it 'maps events to different methods' do
28
+ listener_1 = double('listener')
29
+ listener_2 = double('listener')
30
+ expect(listener_1).to receive(:happy_days).with('hello')
31
+ expect(listener_2).to receive(:sad_days).with('world')
32
+
33
+ command = MyCommand.new
34
+
35
+ command.subscribe(listener_1, :on => :success, :with => :happy_days)
36
+ command.subscribe(listener_2, :on => :failure, :with => :sad_days)
37
+
38
+ command.execute(true)
39
+ command.execute(false)
40
+ end
41
+
42
+ it 'subscribes block can be chained' do
43
+ insider = double('Insider')
44
+
45
+ expect(insider).to receive(:render).with('success')
46
+ expect(insider).to receive(:render).with('failure')
47
+
48
+ command = MyCommand.new
49
+
50
+ command.on(:success) { |message| insider.render('success') }
51
+ .on(:failure) { |message| insider.render('failure') }
52
+
53
+ command.execute(true)
54
+ command.execute(false)
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ class MyPublisher
2
+ include Wisper::Publisher
3
+
4
+ def do_something
5
+ # ...
6
+ broadcast(:bar, self)
7
+ broadcast(:foo, self)
8
+ end
9
+ end
10
+
11
+ describe 'simple publishing' do
12
+ it 'subscribes listener to events' do
13
+ listener = double('listener')
14
+ expect(listener).to receive(:foo).with instance_of MyPublisher
15
+ expect(listener).to receive(:bar).with instance_of MyPublisher
16
+
17
+ my_publisher = MyPublisher.new
18
+ my_publisher.subscribe(listener)
19
+ my_publisher.do_something
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ describe Wisper::TemporaryListeners do
2
+ let(:listener_1) { double('listener', :to_a => nil) } # [1]
3
+ let(:listener_2) { double('listener', :to_a => nil) }
4
+
5
+ let(:publisher) { publisher_class.new }
6
+
7
+ describe '.subscribe' do
8
+ it 'globally subscribes listener for duration of given block' do
9
+
10
+ expect(listener_1).to receive(:success)
11
+ expect(listener_1).to_not receive(:failure)
12
+
13
+ Wisper::TemporaryListeners.subscribe(listener_1) do
14
+ publisher.instance_eval { broadcast(:success) }
15
+ end
16
+
17
+ publisher.instance_eval { broadcast(:failure) }
18
+ end
19
+
20
+ it 'globally subscribes listeners for duration of given block' do
21
+
22
+ expect(listener_1).to receive(:success)
23
+ expect(listener_1).to_not receive(:failure)
24
+
25
+ expect(listener_2).to receive(:success)
26
+ expect(listener_2).to_not receive(:failure)
27
+
28
+ Wisper::TemporaryListeners.subscribe(listener_1, listener_2) do
29
+ publisher.instance_eval { broadcast(:success) }
30
+ end
31
+
32
+ publisher.instance_eval { broadcast(:failure) }
33
+ end
34
+
35
+ it 'is thread safe' do
36
+ num_threads = 20
37
+ (1..num_threads).to_a.map do
38
+ Thread.new do
39
+ Wisper::TemporaryListeners.registrations << Object.new
40
+ expect(Wisper::TemporaryListeners.registrations.size).to eq 1
41
+ end
42
+ end.each(&:join)
43
+
44
+ expect(Wisper::TemporaryListeners.registrations).to be_empty
45
+ end
46
+
47
+ it 'clears registrations when an exception occurs' do
48
+ MyError = Class.new(StandardError)
49
+
50
+ begin
51
+ Wisper::TemporaryListeners.subscribe(listener_1) do
52
+ raise MyError
53
+ end
54
+ rescue MyError
55
+ end
56
+
57
+ expect(Wisper::TemporaryListeners.registrations).to be_empty
58
+ end
59
+
60
+ it 'returns self' do
61
+ expect(Wisper::TemporaryListeners.subscribe {}).to be_an_instance_of(Wisper::TemporaryListeners)
62
+ end
63
+ end
64
+ end
65
+
66
+ # [1] stubbing `to_a` prevents `Double "listener" received unexpected message
67
+ # :to_a with (no args)` on MRI 1.9.2 when a double is passed to `Array()`.
@@ -0,0 +1,93 @@
1
+ module Wisper
2
+ module Broadcasters
3
+
4
+ describe LoggerBroadcaster do
5
+
6
+ describe 'integration tests:' do
7
+ let(:publisher) { publisher_class.new }
8
+ let(:listener) { double }
9
+ let(:logger) { double.as_null_object }
10
+
11
+ it 'broadcasts the event to the listener' do
12
+ publisher.subscribe(listener, :broadcaster => LoggerBroadcaster.new(logger, Wisper::Broadcasters::SendBroadcaster.new))
13
+ expect(listener).to receive(:it_happened).with(1, 2)
14
+ publisher.send(:broadcast, :it_happened, 1, 2)
15
+ end
16
+ end
17
+
18
+ describe 'unit tests:' do
19
+ let(:publisher) { classy_double('Publisher', id: 1) }
20
+ let(:listener) { classy_double('Listener', id: 2) }
21
+ let(:logger) { double('Logger').as_null_object }
22
+ let(:broadcaster) { double('Broadcaster').as_null_object }
23
+ let(:event) { 'thing_created' }
24
+
25
+ subject { LoggerBroadcaster.new(logger, broadcaster) }
26
+
27
+ describe '#broadcast' do
28
+ context 'without arguments' do
29
+ let(:args) { [] }
30
+
31
+ it 'logs published event' do
32
+ expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with no arguments')
33
+ subject.broadcast(listener, publisher, event, args)
34
+ end
35
+
36
+ it 'delegates broadcast to a given broadcaster' do
37
+ expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args)
38
+ subject.broadcast(listener, publisher, event, args)
39
+ end
40
+ end
41
+
42
+ context 'with arguments' do
43
+ let(:args) { [arg_double(id: 3), arg_double(id: 4)] }
44
+
45
+ it 'logs published event and arguments' do
46
+ expect(logger).to receive(:info).with('[WISPER] Publisher#1 published thing_created to Listener#2 with Argument#3, Argument#4')
47
+ subject.broadcast(listener, publisher, event, args)
48
+ end
49
+
50
+ it 'delegates broadcast to a given broadcaster' do
51
+ expect(broadcaster).to receive(:broadcast).with(listener, publisher, event, args)
52
+ subject.broadcast(listener, publisher, event, args)
53
+ end
54
+
55
+ context 'when argument is a hash' do
56
+ let(:args) { [hash] }
57
+ let(:hash) { {key: 'value'} }
58
+
59
+ it 'logs published event and arguments' do
60
+ expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with Hash##{hash.object_id}: #{hash.inspect}")
61
+ subject.broadcast(listener, publisher, event, args)
62
+ end
63
+ end
64
+
65
+ context 'when argument is an integer' do
66
+ let(:args) { [number] }
67
+ let(:number) { 10 }
68
+
69
+ it 'logs published event and arguments' do
70
+ expect(logger).to receive(:info).with("[WISPER] Publisher#1 published thing_created to Listener#2 with #{number.class.name}##{number.object_id}: 10")
71
+ subject.broadcast(listener, publisher, event, args)
72
+ end
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ # provides a way to specify `double.class.name` easily
79
+ def classy_double(klass, options)
80
+ double(klass, options.merge(class: double_class(klass)))
81
+ end
82
+
83
+ def arg_double(options)
84
+ classy_double('Argument', options)
85
+ end
86
+
87
+ def double_class(name)
88
+ double(name: name)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end