siftery-wisper 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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