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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +4 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +129 -0
- data/CONTRIBUTING.md +61 -0
- data/Gemfile +13 -0
- data/README.md +373 -0
- data/Rakefile +10 -0
- data/bin/console +8 -0
- data/bin/setup +6 -0
- data/gem-public_cert.pem +21 -0
- data/lib/wisper.rb +65 -0
- data/lib/wisper/broadcasters/logger_broadcaster.rb +37 -0
- data/lib/wisper/broadcasters/send_broadcaster.rb +9 -0
- data/lib/wisper/configuration.rb +44 -0
- data/lib/wisper/global_listeners.rb +74 -0
- data/lib/wisper/publisher.rb +89 -0
- data/lib/wisper/registration/block.rb +11 -0
- data/lib/wisper/registration/object.rb +77 -0
- data/lib/wisper/registration/registration.rb +19 -0
- data/lib/wisper/temporary_listeners.rb +41 -0
- data/lib/wisper/value_objects/events.rb +61 -0
- data/lib/wisper/value_objects/prefix.rb +29 -0
- data/lib/wisper/version.rb +3 -0
- data/siftery-wisper.gemspec +31 -0
- data/spec/lib/global_listeners_spec.rb +82 -0
- data/spec/lib/integration_spec.rb +56 -0
- data/spec/lib/simple_example_spec.rb +21 -0
- data/spec/lib/temporary_global_listeners_spec.rb +67 -0
- data/spec/lib/wisper/broadcasters/logger_broadcaster_spec.rb +93 -0
- data/spec/lib/wisper/broadcasters/send_broadcaster_spec.rb +28 -0
- data/spec/lib/wisper/configuration/broadcasters_spec.rb +11 -0
- data/spec/lib/wisper/configuration_spec.rb +36 -0
- data/spec/lib/wisper/publisher_spec.rb +331 -0
- data/spec/lib/wisper/registrations/object_spec.rb +14 -0
- data/spec/lib/wisper/value_objects/events_spec.rb +107 -0
- data/spec/lib/wisper/value_objects/prefix_spec.rb +46 -0
- data/spec/lib/wisper_spec.rb +99 -0
- data/spec/spec_helper.rb +24 -0
- metadata +101 -0
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/gem-public_cert.pem
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQUFADBBMRMwEQYDVQQDDAprcmlz
|
3
|
+
LmxlZWNoMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZFgNj
|
4
|
+
b20wHhcNMTQxMTA1MTEwMzQ4WhcNMTUxMTA1MTEwMzQ4WjBBMRMwEQYDVQQDDApr
|
5
|
+
cmlzLmxlZWNoMRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
|
6
|
+
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtW0/UtrFK/tSm
|
7
|
+
uq5HnCUkZAQjnSaZ/h1Tby9s30CDJjDUdizPRdLQCplLDAMHFsAfTyD0Mc+Ez8o9
|
8
|
+
CdTh8EZ4TSf+nokL+SUprpdR6qm6OWU03Ntd+bDCP0+rdqCX82g3N3mnvjR9aD3a
|
9
|
+
+hd9Fhp0WuEyqTNjQ7IlopeUPDW7eYfSwI4bjfRHxsDR1GuZ3j0npxCAgAIN41WH
|
10
|
+
MSTTZhdo0vKEiKZEtMMnT6w6fG/c3XIhVVPGnqy5+IZqBL0SYC+WJL3vC6yUBgqB
|
11
|
+
nrpA/q+b3M69W+q+TkGv0qOnrxln0O7J2pykjoGIxUhhRkiGEldEhy9dxQWubffr
|
12
|
+
hVJ4F0wLAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
13
|
+
BBSzq+x8mwj0ldvNkvjOl44OJG354jAfBgNVHREEGDAWgRRrcmlzLmxlZWNoQGdt
|
14
|
+
YWlsLmNvbTAfBgNVHRIEGDAWgRRrcmlzLmxlZWNoQGdtYWlsLmNvbTANBgkqhkiG
|
15
|
+
9w0BAQUFAAOCAQEAF5M2Md3DcNCrQrDRDLaIzHaMM+RTgfbpgmZ6tU0iEowES18g
|
16
|
+
QWQgrAbFuvQRPETJ2gbL5AC35fEqN80nU+3GhgW/bDYhII5D3PGLMorxhFw1JYLI
|
17
|
+
0Fd7MCE0sImc2rPybYUdpZ6TxvqgPKp+8CzM8vBUrdYd+dSHXit1piViWBcZcJb+
|
18
|
+
EL5Ze4IodjkCPAeHvu2MQieieViLyfB4eG1syvfkxvAXCjFHeQoIFP16vVtcljdF
|
19
|
+
k5cHH/4SGeMuGrSLRsqVvltxVV3AbQAfH8WUos2brjYHsoH5tVPrJ7UcFhzP95oU
|
20
|
+
pEfFMW42smiNTOXpzG6JoIpA11szEHFT5ZS+UQ==
|
21
|
+
-----END CERTIFICATE-----
|
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, &block)
|
31
|
+
if block_given?
|
32
|
+
TemporaryListeners.subscribe(*args, &block)
|
33
|
+
else
|
34
|
+
GlobalListeners.subscribe(*args)
|
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,37 @@
|
|
1
|
+
# Provides a way of wrapping another broadcaster with logging
|
2
|
+
|
3
|
+
module Wisper
|
4
|
+
module Broadcasters
|
5
|
+
class LoggerBroadcaster
|
6
|
+
def initialize(logger, broadcaster)
|
7
|
+
@logger = logger
|
8
|
+
@broadcaster = broadcaster
|
9
|
+
end
|
10
|
+
|
11
|
+
def broadcast(listener, publisher, event, args)
|
12
|
+
@logger.info("[WISPER] #{name(publisher)} published #{event} to #{name(listener)} with #{args_info(args)}")
|
13
|
+
@broadcaster.broadcast(listener, publisher, event, args)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def name(object)
|
19
|
+
id_method = %w(id uuid key object_id).find do |method_name|
|
20
|
+
object.respond_to?(method_name) && object.method(method_name).arity <= 0
|
21
|
+
end
|
22
|
+
id = object.send(id_method)
|
23
|
+
class_name = object.class == Class ? object.name : object.class.name
|
24
|
+
"#{class_name}##{id}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def args_info(args)
|
28
|
+
return 'no arguments' if args.empty?
|
29
|
+
args.map do |arg|
|
30
|
+
arg_string = name(arg)
|
31
|
+
arg_string += ": #{arg.inspect}" if [Numeric, Array, Hash, String].any? {|klass| arg.is_a?(klass) }
|
32
|
+
arg_string
|
33
|
+
end.join(', ')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -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,74 @@
|
|
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)
|
17
|
+
options = listeners.last.is_a?(Hash) ? listeners.pop : {}
|
18
|
+
|
19
|
+
with_mutex do
|
20
|
+
listeners.each do |listener|
|
21
|
+
@registrations << ObjectRegistration.new(listener, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def unsubscribe(*listeners)
|
28
|
+
with_mutex do
|
29
|
+
@registrations.delete_if do |registration|
|
30
|
+
listeners.include?(registration.listener)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def registrations
|
37
|
+
with_mutex { @registrations }
|
38
|
+
end
|
39
|
+
|
40
|
+
def listeners
|
41
|
+
registrations.map(&:listener).freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear
|
45
|
+
with_mutex { @registrations.clear }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.subscribe(*listeners)
|
49
|
+
instance.subscribe(*listeners)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.unsubscribe(*listeners)
|
53
|
+
instance.unsubscribe(*listeners)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.registrations
|
57
|
+
instance.registrations
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.listeners
|
61
|
+
instance.listeners
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.clear
|
65
|
+
instance.clear
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def with_mutex
|
71
|
+
@mutex.synchronize { yield }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
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)
|
42
|
+
registrations.each do | registration |
|
43
|
+
registration.broadcast(clean_event(event), self, *args)
|
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,77 @@
|
|
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
|
13
|
+
end
|
14
|
+
|
15
|
+
def broadcast(event, publisher, *args)
|
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)
|
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
|
+
# @return [Object] broadcaster instance
|
33
|
+
def map_broadcaster
|
34
|
+
key = broadcaster_key
|
35
|
+
value = options[key]
|
36
|
+
return value if value.respond_to?(:broadcast)
|
37
|
+
|
38
|
+
broadcaster_with_options(key, value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Symbol] key to fetch broadcaster by
|
42
|
+
#
|
43
|
+
# @example (setup => key)
|
44
|
+
# publisher.subscribe(Subscriber, async: Wisper::SidekiqBroadcaster.new) # => :async
|
45
|
+
# publisher.subscribe(Subscriber, async: true) # => :async
|
46
|
+
# publisher.subscribe(Subscriber, sidekiq: { queue: 'custom' }) # => :sidekiq
|
47
|
+
# publisher.subscribe(Subscriber) # => :default
|
48
|
+
# publisher.subscribe(Subscriber, broadcaster: Wisper::SidekiqBroadcaster.new) # => :broadcaster
|
49
|
+
# publisher.subscribe(Subscriber, broadcaster: :custom) # => :custom
|
50
|
+
#
|
51
|
+
def broadcaster_key
|
52
|
+
return :async if options.has_key?(:async) && options[:async]
|
53
|
+
return :default unless options.has_key?(:broadcaster)
|
54
|
+
options[:broadcaster].is_a?(Symbol) ? options[:broadcaster] : :broadcaster
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Symbol] key - param to fetch broadcaster by
|
58
|
+
# @param [Boolean, Nil, Hash, Object] value - broadcaster value. Allowed values are the following:
|
59
|
+
# nil # => default broadcaster
|
60
|
+
# false # => default broadcaster
|
61
|
+
# true # => async broadcaster
|
62
|
+
# Broadcaster.new # => returns the provided broadcaster instance
|
63
|
+
# { queue: 'custom' } # => is used when broadcaster is configured as a callable object. In this case
|
64
|
+
# # the given options are passed to broadcaster initializer
|
65
|
+
#
|
66
|
+
# @return [Object] broadcaster instance for the given key / value pair
|
67
|
+
#
|
68
|
+
def broadcaster_with_options(key, value)
|
69
|
+
result = configuration.broadcasters.fetch(key)
|
70
|
+
result.respond_to?(:call) ? result.call(value) : result
|
71
|
+
end
|
72
|
+
|
73
|
+
def configuration
|
74
|
+
Wisper.configuration
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|