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