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,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ require 'coveralls/rake/task'
4
+
5
+ RSpec::Core::RakeTask.new
6
+ Coveralls::RakeTask.new
7
+
8
+ task :test_with_coveralls => [:spec, 'coveralls:push']
9
+
10
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "wisper"
5
+
6
+ require "pry"
7
+ Pry.start
8
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -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-----
@@ -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,9 @@
1
+ module Wisper
2
+ module Broadcasters
3
+ class SendBroadcaster
4
+ def broadcast(listener, publisher, event, args)
5
+ listener.public_send(event, *args)
6
+ end
7
+ end
8
+ end
9
+ 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,11 @@
1
+ # @api private
2
+
3
+ module Wisper
4
+ class BlockRegistration < Registration
5
+ def broadcast(event, publisher, *args)
6
+ if should_broadcast?(event)
7
+ listener.call(*args)
8
+ end
9
+ end
10
+ end
11
+ 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