wisper-compat 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +19 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +4 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +152 -0
  7. data/CONTRIBUTING.md +57 -0
  8. data/Gemfile +10 -0
  9. data/README.md +358 -0
  10. data/Rakefile +6 -0
  11. data/lib/wisper/broadcasters/logger_broadcaster.rb +41 -0
  12. data/lib/wisper/broadcasters/send_broadcaster.rb +9 -0
  13. data/lib/wisper/configuration.rb +44 -0
  14. data/lib/wisper/global_listeners.rb +72 -0
  15. data/lib/wisper/publisher.rb +89 -0
  16. data/lib/wisper/registration/block.rb +11 -0
  17. data/lib/wisper/registration/object.rb +43 -0
  18. data/lib/wisper/registration/registration.rb +18 -0
  19. data/lib/wisper/temporary_listeners.rb +41 -0
  20. data/lib/wisper/value_objects/events.rb +61 -0
  21. data/lib/wisper/value_objects/prefix.rb +29 -0
  22. data/lib/wisper/version.rb +3 -0
  23. data/lib/wisper.rb +65 -0
  24. data/spec/lib/global_listeners_spec.rb +82 -0
  25. data/spec/lib/integration_spec.rb +56 -0
  26. data/spec/lib/simple_example_spec.rb +21 -0
  27. data/spec/lib/temporary_global_listeners_spec.rb +103 -0
  28. data/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb +129 -0
  29. data/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb +68 -0
  30. data/spec/lib/wisper/configuration/broadcasters_spec.rb +11 -0
  31. data/spec/lib/wisper/configuration_spec.rb +36 -0
  32. data/spec/lib/wisper/publisher_spec.rb +311 -0
  33. data/spec/lib/wisper/registrations/object_spec.rb +14 -0
  34. data/spec/lib/wisper/value_objects/events_spec.rb +107 -0
  35. data/spec/lib/wisper/value_objects/prefix_spec.rb +46 -0
  36. data/spec/lib/wisper_spec.rb +99 -0
  37. data/spec/spec_helper.rb +21 -0
  38. data/wisper-compat.gemspec +29 -0
  39. metadata +102 -0
@@ -0,0 +1,44 @@
1
+ require 'forwardable'
2
+
3
+ module Wisper
4
+ class Configuration
5
+ attr_reader :broadcasters
6
+
7
+ def initialize
8
+ @broadcasters = Broadcasters.new
9
+ end
10
+
11
+ # registers a broadcaster, referenced by key
12
+ #
13
+ # @param key [String, #to_s] an arbitrary key
14
+ # @param broadcaster [#broadcast] a broadcaster
15
+ def broadcaster(key, broadcaster)
16
+ @broadcasters[key] = broadcaster
17
+ self
18
+ end
19
+
20
+ # sets the default value for prefixes
21
+ #
22
+ # @param [#to_s] value
23
+ #
24
+ # @return [String]
25
+ def default_prefix=(value)
26
+ ValueObjects::Prefix.default = value
27
+ end
28
+
29
+ class Broadcasters
30
+ extend Forwardable
31
+
32
+ def_delegators :@data, :[], :[]=, :empty?, :include?, :clear, :keys, :to_h
33
+
34
+ def initialize
35
+ @data = {}
36
+ end
37
+
38
+ def fetch(key)
39
+ raise KeyError, "broadcaster not found for #{key}" unless include?(key)
40
+ @data[key]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,72 @@
1
+ # Handles global subscriptions
2
+
3
+ # @api private
4
+
5
+ require 'singleton'
6
+
7
+ module Wisper
8
+ class GlobalListeners
9
+ include Singleton
10
+
11
+ def initialize
12
+ @registrations = Set.new
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def subscribe(*listeners, **options)
17
+ with_mutex do
18
+ listeners.each do |listener|
19
+ @registrations << ObjectRegistration.new(listener, **options)
20
+ end
21
+ end
22
+ self
23
+ end
24
+
25
+ def unsubscribe(*listeners)
26
+ with_mutex do
27
+ @registrations.delete_if do |registration|
28
+ listeners.include?(registration.listener)
29
+ end
30
+ end
31
+ self
32
+ end
33
+
34
+ def registrations
35
+ with_mutex { @registrations }
36
+ end
37
+
38
+ def listeners
39
+ registrations.map(&:listener).freeze
40
+ end
41
+
42
+ def clear
43
+ with_mutex { @registrations.clear }
44
+ end
45
+
46
+ def self.subscribe(*listeners, **options)
47
+ instance.subscribe(*listeners, **options)
48
+ end
49
+
50
+ def self.unsubscribe(*listeners)
51
+ instance.unsubscribe(*listeners)
52
+ end
53
+
54
+ def self.registrations
55
+ instance.registrations
56
+ end
57
+
58
+ def self.listeners
59
+ instance.listeners
60
+ end
61
+
62
+ def self.clear
63
+ instance.clear
64
+ end
65
+
66
+ private
67
+
68
+ def with_mutex
69
+ @mutex.synchronize { yield }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,89 @@
1
+ module Wisper
2
+ module Publisher
3
+ def listeners
4
+ registrations.map(&:listener).freeze
5
+ end
6
+
7
+ # subscribe a listener
8
+ #
9
+ # @example
10
+ # my_publisher.subscribe(MyListener.new)
11
+ #
12
+ # @return [self]
13
+ def subscribe(listener, **options)
14
+ raise ArgumentError, "#{__method__} does not take a block, did you mean to call #on instead?" if block_given?
15
+ local_registrations << ObjectRegistration.new(listener, **options)
16
+ self
17
+ end
18
+
19
+ # subscribe a block
20
+ #
21
+ # @example
22
+ # my_publisher.on(:order_created) { |args| ... }
23
+ #
24
+ # @return [self]
25
+ def on(*events, &block)
26
+ raise ArgumentError, 'must give at least one event' if events.empty?
27
+ raise ArgumentError, 'must pass a block' if !block
28
+ local_registrations << BlockRegistration.new(block, on: events)
29
+ self
30
+ end
31
+
32
+ # broadcasts an event
33
+ #
34
+ # @example
35
+ # def call
36
+ # # ...
37
+ # broadcast(:finished, self)
38
+ # end
39
+ #
40
+ # @return [self]
41
+ def broadcast(event, *args, **kwargs)
42
+ registrations.each do | registration |
43
+ registration.broadcast(clean_event(event), self, *args, **kwargs)
44
+ end
45
+ self
46
+ end
47
+
48
+ alias :publish :broadcast
49
+
50
+ private :broadcast, :publish
51
+
52
+ module ClassMethods
53
+ # subscribe a listener
54
+ #
55
+ # @example
56
+ # MyPublisher.subscribe(MyListener.new)
57
+ #
58
+ def subscribe(listener, **options)
59
+ GlobalListeners.subscribe(listener, **options.merge(:scope => self))
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def local_registrations
66
+ @local_registrations ||= Set.new
67
+ end
68
+
69
+ def global_registrations
70
+ GlobalListeners.registrations
71
+ end
72
+
73
+ def temporary_registrations
74
+ TemporaryListeners.registrations
75
+ end
76
+
77
+ def registrations
78
+ local_registrations + global_registrations + temporary_registrations
79
+ end
80
+
81
+ def clean_event(event)
82
+ event.to_s.gsub('-', '_')
83
+ end
84
+
85
+ def self.included(base)
86
+ base.extend(ClassMethods)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,11 @@
1
+ # @api private
2
+
3
+ module Wisper
4
+ class BlockRegistration < Registration
5
+ def broadcast(event, publisher, *args, **kwargs)
6
+ if should_broadcast?(event)
7
+ listener.call(*args, **kwargs)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ # @api private
2
+
3
+ module Wisper
4
+ class ObjectRegistration < Registration
5
+ attr_reader :with, :prefix, :allowed_classes, :broadcaster
6
+
7
+ def initialize(listener, **options)
8
+ super(listener, **options)
9
+ @with = options[:with]
10
+ @prefix = ValueObjects::Prefix.new options[:prefix]
11
+ @allowed_classes = Array(options[:scope]).map(&:to_s).to_set
12
+ @broadcaster = map_broadcaster(options[:async] || options[:broadcaster])
13
+ end
14
+
15
+ def broadcast(event, publisher, *args, **kwargs)
16
+ method_to_call = map_event_to_method(event)
17
+ if should_broadcast?(event) && listener.respond_to?(method_to_call) && publisher_in_scope?(publisher)
18
+ broadcaster.broadcast(listener, publisher, method_to_call, *args, **kwargs)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def publisher_in_scope?(publisher)
25
+ allowed_classes.empty? || publisher.class.ancestors.any? { |ancestor| allowed_classes.include?(ancestor.to_s) }
26
+ end
27
+
28
+ def map_event_to_method(event)
29
+ prefix + (with || event).to_s
30
+ end
31
+
32
+ def map_broadcaster(value)
33
+ return value if value.respond_to?(:broadcast)
34
+ value = :async if value == true
35
+ value = :default if value == nil
36
+ configuration.broadcasters.fetch(value)
37
+ end
38
+
39
+ def configuration
40
+ Wisper.configuration
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ # @api private
2
+
3
+ module Wisper
4
+ class Registration
5
+ attr_reader :on, :listener
6
+
7
+ def initialize(listener, **options)
8
+ @listener = listener
9
+ @on = ValueObjects::Events.new options[:on]
10
+ end
11
+
12
+ private
13
+
14
+ def should_broadcast?(event)
15
+ on.include? event
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,41 @@
1
+ # Handles temporary global subscriptions
2
+
3
+ # @api private
4
+
5
+ module Wisper
6
+ class TemporaryListeners
7
+ def self.subscribe(*listeners, **options, &block)
8
+ new.subscribe(*listeners, **options, &block)
9
+ end
10
+
11
+ def self.registrations
12
+ new.registrations
13
+ end
14
+
15
+ def subscribe(*listeners, **options, &_block)
16
+ new_registrations = build_registrations(*listeners, **options)
17
+
18
+ begin
19
+ registrations.merge new_registrations
20
+ yield
21
+ ensure
22
+ registrations.subtract new_registrations
23
+ end
24
+ self
25
+ end
26
+
27
+ def registrations
28
+ Thread.current[key] ||= Set.new
29
+ end
30
+
31
+ private
32
+
33
+ def build_registrations(*listeners, **options)
34
+ listeners.map { |listener| ObjectRegistration.new(listener, **options) }
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 = "4.0.0"
3
+ end
data/lib/wisper.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'set'
2
+ require 'wisper/version'
3
+ require 'wisper/configuration'
4
+ require 'wisper/publisher'
5
+ require 'wisper/value_objects/prefix'
6
+ require 'wisper/value_objects/events'
7
+ require 'wisper/registration/registration'
8
+ require 'wisper/registration/object'
9
+ require 'wisper/registration/block'
10
+ require 'wisper/global_listeners'
11
+ require 'wisper/temporary_listeners'
12
+ require 'wisper/broadcasters/send_broadcaster'
13
+ require 'wisper/broadcasters/logger_broadcaster'
14
+
15
+ module Wisper
16
+ # Examples:
17
+ #
18
+ # Wisper.subscribe(AuditRecorder.new)
19
+ #
20
+ # Wisper.subscribe(AuditRecorder.new, StatsRecorder.new)
21
+ #
22
+ # Wisper.subscribe(AuditRecorder.new, on: 'order_created')
23
+ #
24
+ # Wisper.subscribe(AuditRecorder.new, scope: 'MyPublisher')
25
+ #
26
+ # Wisper.subscribe(AuditRecorder.new, StatsRecorder.new) do
27
+ # # ..
28
+ # end
29
+ #
30
+ def self.subscribe(*args, **kwargs, &block)
31
+ if block_given?
32
+ TemporaryListeners.subscribe(*args, **kwargs, &block)
33
+ else
34
+ GlobalListeners.subscribe(*args, **kwargs)
35
+ end
36
+ end
37
+
38
+ def self.unsubscribe(*listeners)
39
+ GlobalListeners.unsubscribe(*listeners)
40
+ end
41
+
42
+ def self.publisher
43
+ Publisher
44
+ end
45
+
46
+ def self.clear
47
+ GlobalListeners.clear
48
+ end
49
+
50
+ def self.configure
51
+ yield(configuration)
52
+ end
53
+
54
+ def self.configuration
55
+ @configuration ||= Configuration.new
56
+ end
57
+
58
+ def self.setup
59
+ configure do |config|
60
+ config.broadcaster(:default, Broadcasters::SendBroadcaster.new)
61
+ end
62
+ end
63
+ end
64
+
65
+ Wisper.setup
@@ -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