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.
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